redux-saga-profile
redux-saga,一个响亮的名字,虽然在上一篇文章中介绍过,但是看完源码我还是忍不住郑重的重新介绍一下。这是管理计划的“副作用”。虽然该框架大多数情况下都是作为redux中间件使用的,但从基本使用上来说,它并不依赖于其他库,可以单独使用。使用它来管理程序副作用具有以下好处:
- 更好的测试
- 更清晰的代码逻辑
- 轻松管理副作用的激活和取消
PS:这个库的代码写得真的很好。充满了计算机专业术语,让我感觉舒服又熟悉。 Fork、通道、任务、io概念、信号量、缓冲区,仅基于概念层,saga胜太多
一些概念
效果
官网已经多次强调,Effect是saga中间件的执行单元。它是通过内部 API 生成的。它是一个简单的对象,包含一些信息,例如类型属性。根据这些信息,saga中间件可能会选择向下执行。阻止、采取行动等操作(take、分叉、放)
传奇
什么是saga,我个人的看法是它是效果的集合,可以分为work saga和root saga。 root saga 负责分发操作,worker saga 负责响应特定操作。两者之间的组合嵌套可以基于操作的生成器函数语法
function * worksaga(getstate){
try{
yield call(some async op);
yield put({type:'SOME ACTION WHEN SUCCESS'});
}catch(err){
yield put({type:'SOME ACTION WHEN FAIL'})
}
}
function* rootsaga() {
yield* takeEvery("SOME ACTION", worksaga);
}
任务
每个 saga 对应一个任务,用于管理迭代器操作。任务有很多种类型。 Main任务主要遵循整个Main流程。 Fork任务是通过fork创建的任务。父任务管理主任务和各种分支。
任务过程
主要执行逻辑集中在proc中,它定义了saga中间件逻辑来执行Effect。从 Generator 函数中获取迭代器可以控制函数,并利用辅助函数之间的相互迭代来不断调用 iterator.next
。频道
保存任务回调并触发任务回调,channel.take记录回调,channel.put匹配并监听当前action的回调,触发
(源码已删除)
put(input) {
const takers = (currentTakers = nextTakers)
for (let i = 0, len = takers.length; i < len; i++) {
const taker = takers[i]
if (taker[MATCH](input)) {
taker.cancel()
taker(input)
}
}
},
take(cb, matcher = matchers.wildcard) {
cb[MATCH] = matcher
nextTakers.push(cb)
cb.cancel = once(() => {
remove(nextTakers, cb)
})
},
上面列出一些概念是为了更好的理解下面源码的解读,源码比较复杂加上本人水平有限,不可能解释的太详细,也没有必要解释太多详细解释。
源代码
一般来说,按照惯例,使用前应该先检查一下源代码,但这在上一篇文章中已经提到过。如果你不熟悉,你可以读一遍再读一遍。这里就不讲了,直接从入口开始分析
入口
function sagaMiddleware({ getState, dispatch }) {
boundRunSaga = runSaga.bind(null, {
...options,
context,
channel,
dispatch,
getState,
sagaMonitor,
})
return next => action => {
if (sagaMonitor && sagaMonitor.actionDispatched) {
sagaMonitor.actionDispatched(action)
}
const result = next(action) // hit reducers
channel.put(action)
return result
}
saga入口仍然接收redux传递给中间件的两个API。事实上,外面有一层工厂函数将自身与环境解耦。这里就省略了。入口与 boundRunSaga
功能相关。就是sagaMiddleware.run
调用的函数,返回的高阶函数的主要逻辑是,当action被dispatch时,照常调用其他中间件封装的dispatch函数,即下面的函数,但是之前返回结果,使用channel.put(action)
唤醒saga。中间件本身的逻辑相当于打开一个流程来独立于redux流程来处理副作用。
如前所述,
激活监视操作的回调。这些回调请求是什么时候注册的?
runSaga 和 proc
runSaga 是 sagaMiddleware.run 调用的函数
在runSaga中,首先获取传递给根saga的迭代器
const iterator = saga(...args);
然后生成环境,可以认为是任务流程执行的系统环境
const env = {
channel,
dispatch: wrapSagaDispatch(dispatch),
getState,
sagaMonitor,
onError,
finalizeRunEffect,
};
然后立即运行一个函数,该函数用于创建父任务来管理 root saga 并监控整体流程。它上面有一些控制功能,比如取消,然后执行saga函数迭代器并根据迭代器返回。效果类型继续执行
immediately(() => {
const task = proc(
env,
iterator,
context,
effectId,
getMetaInfo(saga),
/* isRoot */ true,
undefined
);
if (sagaMonitor) {
sagaMonitor.effectResolved(effectId, task);
}
return task;
});
(源码已删除)
export default function proc(
env,
iterator,
parentContext,
parentEffectId,
meta,
isRoot,
cont
) {
next.cancel = noop;
/** Creates a main task to track the main flow */
const mainTask = { meta, cancel: cancelMain, status: RUNNING };
/**
Creates a new task descriptor for this generator.
A task is the aggregation of it's mainTask and all it's forked tasks.
**/
const task = newTask(
env,
mainTask,
parentContext,
parentEffectId,
meta,
isRoot,
cont
);
const executingContext = {
task,
digestEffect,
};
/**
cancellation of the main task. We'll simply resume the Generator with a TASK_CANCEL
**/
function cancelMain() {
if (mainTask.status === RUNNING) {
mainTask.status = CANCELLED;
next(TASK_CANCEL);
}
}
/**
attaches cancellation logic to this task's continuation
this will permit cancellation to propagate down the call chain
**/
if (cont) {
cont.cancel = task.cancel;
}
// kicks up the generator
next();
// then return the task descriptor to the caller
return task;
}
next 和 EffectRunner
之后,Effect 命令会在 saga 中间件中执行。它会根据Effect.type类型找到对应的EffectRunner并运行这个runner函数
function runEffect(effect, effectId, currCb) {
if (is.promise(effect)) {
resolvePromise(effect, currCb);
} else if (is.iterator(effect)) {
// resolve iterator
proc(env, effect, task.context, effectId, meta, /* isRoot */ false, currCb);
} else if (effect && effect[IO]) {
const effectRunner = effectRunnerMap[effect.type];
effectRunner(env, effect.payload, currCb, executingContext);
} else {
// anything else returned as is
currCb(effect);
}
}
其中effectRunnerMap[effect.type]
就是找到主教并处决,见effectRunnerMap
const effectRunnerMap = {
[effectTypes.TAKE]: runTakeEffect,
[effectTypes.PUT]: runPutEffect,
[effectTypes.ALL]: runAllEffect,
[effectTypes.RACE]: runRaceEffect,
[effectTypes.CALL]: runCallEffect,
[effectTypes.CPS]: runCPSEffect,
[effectTypes.FORK]: runForkEffect,
[effectTypes.JOIN]: runJoinEffect,
[effectTypes.CANCEL]: runCancelEffect,
[effectTypes.SELECT]: runSelectEffect,
[effectTypes.ACTION_CHANNEL]: runChannelEffect,
[effectTypes.CANCELLED]: runCancelledEffect,
[effectTypes.FLUSH]: runFlushEffect,
[effectTypes.GET_CONTEXT]: runGetContextEffect,
[effectTypes.SET_CONTEXT]: runSetContextEffect,
};
是我们所知道的一些效果类型对应的跑步者,我们来看看一些常见的跑步者
- 动
显然是直接send(action),与常规直接send的区别在于put(action)返回一个效果,这对于测试很有用
- 打电话
function runCallEffect(env, { context, fn, args }, cb, { task }) {
// catch synchronous failures; see #152
try {
const result = fn.apply(context, args);
if (is.promise(result)) {
resolvePromise(result, cb);
return;
}
if (is.iterator(result)) {
// resolve iterator
proc(
env,
result,
task.context,
currentEffectId,
getMetaInfo(fn),
/* isRoot */ false,
cb
);
return;
}
cb(result);
} catch (error) {
cb(error, true);
}
}
调用的逻辑可以猜到,即把迭代器的控制包装在cb中,然后在promise解决的时候执行,相当于阻塞。
- take
function runTakeEffect(env, { channel = env.channel, pattern, maybe }, cb) {
const takeCb = (input) => {
if (input instanceof Error) {
cb(input, true);
return;
}
if (isEnd(input) && !maybe) {
cb(TERMINATE);
return;
}
cb(input);
};
try {
channel.take(takeCb, is.notUndef(pattern) ? matcher(pattern) : null);
} catch (err) {
cb(err, true);
return;
}
cb.cancel = takeCb.cancel;
}
take正在等待行动的到来。据说这是控制权的逆转。基本上它的目的是在通道中注册take的回调并等待操作到达时唤醒通道
-
叉子
fork 首先返回结果,而不是阻塞迭代器的执行。显然需要一个新的任务来负责fork过程,但是它并没有等待任务完成,而是立即返回
function runForkEffect(
env,
{ context, fn, args, detached },
cb,
{ task: parent }
) {
const taskIterator = createTaskIterator({ context, fn, args });
const meta = getIteratorMetaInfo(taskIterator, fn);
immediately(() => {
const child = proc(
env,
taskIterator,
parent.context,
currentEffectId,
meta,
detached,
undefined
);
if (detached) {
cb(child);
} else {
if (child.isRunning()) {
parent.queue.addTask(child);
cb(child);
} else if (child.isAborted()) {
parent.queue.abort(child.error());
} else {
cb(child);
}
}
});
// Fork effects are non cancellables
}
- take每
take Every 是高层API,底层实现是take+fork,源码逻辑不太好看,官网上的例子比较清晰
export function takeEvery(pattern, saga) {
function* takeEveryHelper() {
while (true) {
yield take(pattern);
yield fork(saga);
}
}
return fork(takeEveryHelper);
}
等待aciton启动,分配任务执行并继续监听action的启动,无需等待返回
总结
- redux-saga通过自己搭建的一系列副作用处理系统独立工作
- 每个 saga 函数都代表一些需要执行的副作用。 saga 函数在内部表示为任务。该任务负责监视函数的执行,并具有随时执行与其关联的取消的方法。任务执行逻辑由proc 处理
- proc 是处理每个任务的地方。首先,获取任务的迭代器。内置的next函数中根据每个yield语句返回的Effect选择对应的EffectRunner执行,完成阻塞逻辑
- Effect是最小的操作单元,可以理解为传递给saga中间件的指令
- take Effect 注册到通道,等待操作到达,然后执行迭代器
- fork效果生成新的任务管理器并立即返回,不会阻塞迭代器
- take 一切都建立在 fork 和 take 之上。它等待操作的到来并分出要执行的任务。由于 fork 不会阻塞迭代器,因此它可以响应任何操作
另外,取消迭代器是saga中相对复杂的部分,这里不再赘述。我会看看将来是否有可能单独出版一期。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。