1.redux-saga是什么?
顾名思义saga与redux相关,redux-saga是一个以redux中间件形式存在的一个库,主要是为了更优雅地 管理 Redux 应用程序中的 副作用(Side Effects),执行更高效,测试更简单,在处理故障时更容易。同样的,从logo也可以看出saga于redux的关系。
关于saga的由来,它出自康奈尔大学的一篇论文(链接),是为了解决分布式系统中的长时运行事务(LLT)的数据一致性的问题。
2.什么是SideEffects?
映射在 Javascript 程序中,Side Effects 主要指的就是:异步网络请求、本地读取 localStorage/Cookie 等外界操作:
在 Web 应用,侧重点在于 Side Effects 的优雅管理(manage),而不是 消除(eliminate)。
3.saga与thunk有什么不同?
首先,比较了saga与thunk的包体积大小,二者相差10倍之多。
无论是redux-thunk也好还是redux-saga也好,都是redux的中间件。而redux作为主体,为每个中间件,提供了统一格式,下发getState、dispatch,以及调用dispatch,收集action。
//compose.js
function compose(..funcs) {
if (funcs.length === 0) {
retyrb arg => arg
}
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
//applyMiddleware.js
function applyMiddleware(...middlewares) {
return (createStore) => (reducer, preloaderState, enhancer) => {
const store = createStore(reducer, preloadedState, enhancer)
let dispatch = store.dispatch
let chain = []
const middlewareAPI = {
getState: store.getState,
diapatch: (action) => dispatch(action)
}
chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}
接着,我们先再来看看thunk函数,在阮大大的文章中有介绍到thunk函数:
function f(m){
return m * 2;
}
f(x + 5);
// 等同于
var thunk = function () {
return x + 5;
};
function f(thunk){
return thunk() * 2;
}
编译器的"传名调用"实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫做 Thunk 函数。在 JavaScript 语言中,Thunk 函数替换的不是表达式,而是多参数函数,将其替换成单参数的版本,且只接受回调函数作为参数。
然后我们再来看看thunk的源码
function createThunkMiddleware(extraArgument) {
//dispath,可以用来dispatch新的action
//getState,可以用于访问当前的state
return ({dispatch, getState}) => (next) => (action) => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
redux-thunk是个中间件,去监控传入系统中的每一个action
,如果是个函数的话,那么它就会调用那个函数。这就是redux-thunk
的职责。redux-thunk 选择以 middleware 的形式来增强 redux store 的 dispatch 方法(即:支持了 dispatch(function)
),从而在拥有了异步获取数据能力的同时,又可以进一步将数据获取相关的业务逻辑 从 View 层分离出去。
接着来看看redux-saga,saga模式是以命令/答复的形式与各个saga之间进行通讯,当接收到指令时会执行对应的saga,如图所示:
saga模式将各个服务隔离开,采用集中分布式事务的编排,能够避免服务之间的循环依赖并有利于测试。同时减少了参与者的复杂性,因为他们只需要执行/回复命令。但是,saga会产生很多无用的action.type。
综上,redux-thunk与redux-saga都是redux的中间件,但是他们的设计思想不同,因此他们的使用方法也不同,首先来看redux-thunk的写法:
// action.js
// ---------
// actionCreator(e.g. fetchData) 返回 function
// function 中包含了业务数据请求代码逻辑
// 以回调的方式,分别处理请求成功和请求失败的情况
export function fetchData(someValue) {
return (dispatch, getState) => {
myAjaxLib.post("/someEndpoint", { data: someValue })
.then(response => dispatch({ type: "REQUEST_SUCCEEDED", payload: response })
.catch(error => dispatch({ type: "REQUEST_FAILED", error: error });
};
}
// component.js
// ------------
// View 层 dispatch(fn) 触发异步请求
// 这里省略部分代码
this.props.dispatch(fetchData({ hello: 'saga' }));
再来看redux-saga的写法,以及架构图:
// saga.js
// -------
// worker saga
// 它是一个 generator function
// fn 中同样包含了业务数据请求代码逻辑
// 但是代码的执行逻辑:看似同步 (synchronous-looking)
function* fetchData(action) {
const { payload: { someValue } } = action;
try {
const result = yield call(myAjaxLib.post, "/someEndpoint", { data: someValue });
yield put({ type: "REQUEST_SUCCEEDED", payload: response });
} catch (error) {
yield put({ type: "REQUEST_FAILED", error: error });
}
}
// watcher saga
// 监听每一次 dispatch(action)
// 如果 action.type === 'REQUEST',那么执行 fetchData
export function* watchFetchData() {
yield takeEvery('REQUEST', fetchData);
}
// component.js
// -------
// View 层 dispatch(action) 触发异步请求
// 这里的 action 依然可以是一个 plain object
this.props.dispatch({
type: 'REQUEST',
payload: {
someValue: { hello: 'saga' }
}
});
综上可以看出,redux-saga相较于redux-thunk有这几点不同
1.数据获取相关的业务逻辑被转移到单独的saga.js中,不再是参杂在action.js或component.js中。
2.每一个saga都是一个generator function,代码采用同步书写的方式来处理异步逻辑,代码变得更易读。
4.学习saga使用
saga总共提供了两个MiddlewareAPI,为createSagaMiddleware、middleware.run。
createSagaMiddleware(options): 创建一个 Redux middleware,并将 Sagas 连接到 Redux Store。其中options支持的选项有(可不提供):
-
sagaMontior:用于接收middleware传递的监视事件。
-
emmiter:用于从redux向redux-saga进给actions
-
logger:自定义日志方法(默认情况下,middleware会把所有的错误和警告记录到控制台中)。
-
onError:当提供该方法时,middleware将带着Sagas中未被捕获的错误调用它。
middleware.run(saga, ...args): 动态地运行 saga。只能用于在 applyMiddleware 阶段之后执行Saga,其中args为提供给saga的参数。
在安装完所有依赖后,首先将store 与saga的关联,并在最后去执行rootsaga。
import { createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
import rootSaga from './sagas'
import rootReducer from './reducers'
const sgagMiddleware = createSagaMiddleware();
const enhancer = applyMiddleware(sagaMiddleware);
const store = createStore(rootReducer, enhancer);
//执行rootSaga,通常是程序的初始化操作。
sagaMiddleWare.run(rootSaga);
然后,再介绍saga中比较重要的几个概念,分别为:Task、Channel、Buffer、SagaMonitor。
1.Task
Task 接口指定了通过 fork
,middleare.run
或 runSaga
运行 Saga 的结果,并提供了相应的函数方法。
2.Channel
channel 是用于在任务间发送和接收消息的对象。在被感兴趣的接收者请求之前,来自发送者的消息将被放入(put)队列;在信息可用之前,已注册的接收者将被放入队列。
Channel 接口定义了 3 个方法:take
,put
和 close
Channel.take(callback):
用于注册一个 taker。
Channel.put(message):
用于在 buffer 上放入消息。
Channel.flush(callback):
用于从 channel 中提取所有被缓存的消息。
Channel.close():
关闭 channel,意味着不再允许做放入操作。
3.Buffer
用于为 channel 实现缓存策略。Buffer 接口定义了 3 个方法:isEmpty
,put
和 take
isEmpty()
: 如果缓存中没有消息则返回。每当注册了新的 taker 时,channel 都会调用该方法。put(message)
: 用于往缓存中放入新的消息。请注意,缓存可以选择不存储消息。(例如,一个 dropping buffer 可以丢弃超过给定限制的任何新消息)take()
:用于检索任何被缓存的消息。请注意,此方法的行为必须与isEmpty
一致。
4.SagaMonitor
用于由 middleware 发起监视(monitor)事件。实际上,middleware 发起 5 个事件:
- 当一个 effect 被触发时(通过
yield someEffect
),middleware 调用sagaMonitor.effectTriggered
- 如果该 effect 成功地被 resolve,则 middleware 调用
sagaMonitor.effectResolved
- 如果该 effect 因一个错误被 reject,则 middleware 调用
sagaMonitor.effectRejected
- 如果该 effect 被取消,则 middleware 调用
sagaMonitor.effectCancelled
- 最后,当 Redux action 被发起时,middleware 调用
sagaMonitor.actionDispatched
接着再来介绍redux-saga中的Effect创建器,在redux-saga中主要通过effect来维护,关于Effect的描述如下:
effect 本质上是一个普通对象,包含着一些指令信息,这些指令最终会被 saga middleware 解释并执行(实际上是一个发布订阅模式)。源码解析可参考文章(juejin.cn/post/688522…
以take为例,take是一个Effect创建器,用以创建Effect,源码如下:
官方解释:
- 以下每个Effect创建函数都会返回一个普通 Javascript 对象(plain JavaScript object),并且不会执行任何其它操作。
- 执行是由 middleware 在上述迭代过程中进行的。
- middleware 会检查每个 Effect 的描述信息,并进行相应的操作
接下去简单解释一下各个Effect创建器以及Effect组合器、辅助函数的作用:
Take: 创建一个 Effect 描述信息,用来命令 middleware 在 Store 上等待指定的 action。 在发起与 pattern
匹配的 action 之前,Generator 将暂停。
Put: 创建一个 Effect 描述信息,用来命令 middleware 向 Store 发起一个 action。 这个 effect 是非阻塞型的,并且所有向下游抛出的错误(例如在 reducer 中),都不会冒泡回到 saga 当中。
Call: 创建一个 Effect 描述信息,用来命令 middleware 以参数 args
调用函数 fn
。
Apply: 类似Call。
Fork: 创建一个 Effect 描述信息,用来命令 middleware 以 非阻塞调用 的形式执行 fn
。
Spawn: 与fork类似,但创建的是被分离的任务。被分离的任务与其父级任务保持独立。
Join: 创建一个 Effect 描述信息,用来命令 middleware 等待之前的一个分叉任务的结果。
Cancel:创建一个 Effect,用以取消任务。
Select: 创建一个 Effect,用来命令 middleware 在当前 Store 的 state 上调用指定的选择器(即返回 selector(getState(), ...args) 的结果)。
ActionChannel: 创建一个 Effect,用来命令 middleware 通过一个事件 channel 对匹配 pattern
的 action 进行排序。
Flush: 创建一个 Effect,用来命令 middleware 从 channel 中冲除所有被缓存的数据。被冲除的数据会返回至 saga,这样便可以在需要的时候再次被利用。
Cancelled: 创建一个 Effect,用来命令 middleware 返回该 generator 是否已经被取消。
setContext: 创建一个 effect,用来命令 middleware 更新其自身的上下文。
getContext: 创建一个 effect,用来命令 middleware 返回 saga 的上下文中的一个特定属性。
Effect组合器
Race: 创建一个 Effect 描述信息,用来命令 middleware 在多个 Effect 间运行 竞赛(Race)(与 Promise.race([...]) 的行为类似)。
All: 创建一个 Effect 描述信息,用来命令 middleware 并行地运行多个 Effect,并等待它们全部完成。这是与标准的 Promise#all 相当对应的 API。
Saga辅助函数
TakeEvery: 在发起(dispatch)到 Store 并且匹配 pattern 的每一个 action 上派生一个 saga。
TakeLatest: 在发起到 Store 并且匹配 pattern 的每一个 action 上派生一个 saga。并自动取消之前所有已经启动但仍在执行中的 saga 任务。
TakeLeading: 在发起到 Store 并且匹配 pattern 的每一个 action 上派生一个 saga。 它将在派生一次任务之后阻塞,直到派生的 saga 完成,然后又再次开始监听指定的 pattern。
Throttle: 在发起到 Store 并且匹配 pattern 的一个 action 上派生一个 saga。 它在派生一次任务之后,仍然将新传入的 action 接收到底层的 buffer 中,至多保留(最近的)一个。但与此同时,它在 ms 毫秒内将暂停派生新的任务 —— 这也就是它被命名为节流阀(throttle)的原因。其用途,是在处理任务时,无视给定的时长内新传入的 action。
5.Redux-Saga测试
由于redux-saga将每个副作用细化到一个较小的维度,并使各个服务之间的耦合性较小。因此非常利于进行单元测试,案例如下:
function* callApi(url) {
const someValue = yield select(somethingFromState)
try {
const result = yield call(myApi, url, someValue)
yield put(success(result.json()));
return result.status;
} catch (e) {
yield put(error(e));
return -1;
}
}
const dispatched = [];
const saga = runSaga({
dispatch: (action) => dispatched.push(action),
getState: () => ({ value: 'test' }),
}, callApi, 'http://url');
import sinon from 'sinon';
import * as api from './api';
test('callApi', async (assert) => {
const dispatched = [];
sinon.stub(api, 'myApi').callsFake(() => ({
json: () => ({
some: 'value'
})
}));
const url = 'http://url';
const result = await runSaga({
dispatch: (action) => dispatched.push(action),
getState: () => ({ state: 'test' }),
}, callApi, url).done;
assert.true(myApi.calledWith(url, somethingFromState({ state: 'test' })));
assert.deepEqual(dispatched, [success({ some: 'value' })]);
});
最后再推荐两个,阅读官方文档后觉得比较好的小技巧的使用。
6.Redux-Saga使用技巧
1.ajax重试
import { call, put, take, delay, delay } from 'redux-saga/effects'
function* updateApi(data) {
while (true) {
try {
const apiResponse = yield call (apiRequest, { data })
return apiResponse;
} catch(error) {
yield put({
type: 'UPDATE_RETRY',
error
})
yield delay(2000)
}
}
}
function* updateResource({ data }) {
const apiResponse = yield call(updateApi, data);
yield put({
type: 'UPDATE_SUCCESS',
payload: apiResponse.body,
});
}
export function* watchUpdateResource() {
yield takeLatest('UPDATE_START', updateResource);
}
2.撤销
import { take, put, call, spawn, race, delay } from 'redux-saga/effects'
import { updateThreadApi, actions } from 'somewhere'
function* onArchive(action) {
const { threadId } = action
const undoId =`UNDO_ARCHIVE_${threadId}`
const thread = { id: threadId, archived: true}
yield put(actions.showUndo(undoId))
yield put(actions.updateThread(thread))
const { undo, archive } = yield race({
undo: take(action => action.type === 'UNDO' && action.undoId === undoId),
archive: delay(5000)
})
yield put(actions.hideUndo(undoId))
if (undo) {
yield put(actions.updateThread({ id: threadId, archived: false}))
} else if (archive) {
yield call(updateThreadApi,thread)
}
}
function* main() {
while (true) {
const action = yield take(`ARCHIVE_THREAD`)
yield spawn(onArchive, action)
}
}
参考文章:
1.Redux-Saga 漫谈
2.Saga Pattern
3.Redux-Saga官方文档
4.Why saga
5.手写Redux-Saga源码
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!