开始前最好看过我写过的redux-saga学习笔记
注:从redux-saga/effect引入的方法直接可以去saga源码中effectRunnerMap.js找对应的方法,那里是该方法的主要功能逻辑
all方法功能解析
示例代码:
执行all方法时主要执行过程
如图所示
function output(count){
return count
}
function* rootSaga() {
const [a,b]= yield all([call(output,0),call(output,1)])
console.error('测试',a,b)
yield takeEvery('INCREMENT_ASYNC', incrementAsync)
}
从源码入手
runAllEffect方法源码
我们直接看runAllEffect
方法在这之前的过程略过。runAllEffect
方法源码:
function runAllEffect(env, effects, cb, { digestEffect }) {
const effectId = currentEffectId
const keys = Object.keys(effects)
if (keys.length === 0) {
cb(is.array(effects) ? [] : {})
return
}
const childCallbacks = createAllStyleChildCallbacks(effects, cb)
keys.forEach(key => {
digestEffect(effects[key], effectId, childCallbacks[key], key)
})
}
看一下这段代码const childCallbacks = createAllStyleChildCallbacks(effects, cb)
,我们看一下createAllStyleChildCallbacks
方法
createAllStyleChildCallbacks方法源码及功能解析
function createAllStyleChildCallbacks(shape, parentCallback) {
//shape为all方法的第一入参[call(output,0),call(output,1)]
//parentCallback则为闭包了迭代器(rootsaga生成器函数返回的)的next方法。当shape里的方法全部执行完调用它继续迭代
const keys = Object.keys(shape)
const totalCount = keys.length
if (process.env.NODE_ENV !== 'production') {
check(totalCount, c => c > 0, 'createAllStyleChildCallbacks: get an empty array or object')
}
let completedCount = 0
let completed
const results = is.array(shape) ? createEmptyArray(totalCount) : {}
const childCallbacks = {}
function checkEnd() {
if (completedCount === totalCount) {
completed = true
console.log(results,'childCallbacks')
parentCallback(results)
}
}
keys.forEach(key => {
const chCbAtKey = (res, isErr) => {
if (completed) {
return
}
if (isErr || shouldComplete(res)) {
parentCallback.cancel()
parentCallback(res, isErr)
} else {
results[key] = res
completedCount++
checkEnd()
}
}
chCbAtKey.cancel = noop
childCallbacks[key] = chCbAtKey
})
parentCallback.cancel = () => {
if (!completed) {
completed = true
keys.forEach(key => childCallbacks[key].cancel())
}
}
return childCallbacks
}
获取到[call(output,0),call(output,1)]
的key数组,我们知道数组的key就是其元素的索引。遍历这个key数组 源码中定义了一个chCbAtKey
方法,这个方法作为执行完示例代码output
方法之后的回调,一般这个回调为包装后的next
方法,但在这里不是。
遍历过程中将chCbAtKey
方法和对应的key添加到childCallbacks
对象,最后返回此对象。
chCbAtKey
方法将在
//在runAllEffect方法中
keys.forEach(key => {
digestEffect(effects[key], effectId, childCallbacks[key], key)
})
执行此代码执行,disgestEffect
方法先会处理[call(output,0),call(output,1)]
里的每一个元素,在这里也就是call
调用这个output
方法(call方法下面会说)。接着执行方法中的第三参数childCallbacks[key]
,我们知道这便是存入到childCallbacks
里的chCbAtKey
。
回到chCbAtKey
方法:
const chCbAtKey = (res, isErr) => {
if (completed) {
return
}
if (isErr || shouldComplete(res)) {
parentCallback.cancel()
parentCallback(res, isErr)
} else {
results[key] = res
completedCount++
checkEnd()
}
}
首先看这个方法的参数,其形式与proc
里的next
一样,所以也可以看出它暂替next
成为digestEffect
处理完用户自定义的方法之后的回调方法。chCbAtKey
方法的第一参res
就是用户自定义的方法的返回值,可以看看proc
方法中runEffect
最后一种判断情况,当拿到的effect
(这个effect
的就是迭代器返回对象里的value
)判断是否是promise
,是否是迭代器,是否是saga
的effect
,如果上述情况都不是即返回的就是一个值,随后将这个effect
传入到next
中。
终上所述,我们得知回调chCbAtKey
方法第一参是怎么得到的,它是什么。
接着往下看看else里的逻辑,用results
数组或是对象存储这个res
,results
最终会存储所有传入给all
方法我们自定义的方法的返回值。接着completeCount
自加用来记录遍历次数。
看看checkEnd
方法:
function checkEnd() {
if (completedCount === totalCount) {
completed = true
parentCallback(results)
}
}
首先做下判断,判断遍历次数是否等于遍历数组的长度,如果等于则证明已经迭代完。
parentCallback
方法既是runAllEffect
方法传入的包装过的next
方法。将results
传入了parentCallback
方法。方法内部简单逻辑为:
function next(arg, isErr) {
result = iterator.next(arg)
}
所以当代码const [a,b]= yield all([call(output,0),call(output,1)])
迭代后即const [a,b]=results
,通过数组解构a
,b
分别获取了results
内的两个元素。
至此all
方法的基本执行逻辑结束。
all方法的简单功能测试对promise的处理方式
我们把上述示例代码output
方法改动一下,其它的不动,代码如下
function output(count){
const promise=new Promise((resolve)=>{
setTimeout(()=>{
console.log('haya',count++)
resolve(count)
},2000)
})
return promise
}
我们将output
方法返回一个promise
,内部有个setTimeout
延迟调用这里是2s。
执行后我们发现两秒后打印出两个结果,这证明一件事,就是all
方法没有将两个promise
串联成一起,造成同步阻塞,而是并发。
回到all
方法的内部:
keys.forEach(key => {
digestEffect(effects[key], effectId, childCallbacks[key], key)
})
我们得知digestEffect
方法被直接调用,一般而言此方法是随着next
被调用而调用,digestEffect
方法最终会调用runEffect
方法,其方法内部有对promise
的处理,处理逻辑是将迭代器的下次迭代方法next
方法放到promise
的then
回调中。
由此可推测当将childCallbacks[key]
(chCbAtKey
)放入到output
返回的promise
的then
中,digestEffect
就执行结束,然后继续遍历执行下一个。这就将promise
并发处理了,而非是then
串联
我们看一种令其同步阻塞的一种使用方式:
function* rootSaga() {
yield* [call(output,0),call(output,1)]
yield takeEvery('INCREMENT_ASYNC', incrementAsync)
}
这里将const [a,b]= yield all([call(output,0),call(output,1)])
改成yield* [call(output,0),call(output,1)]
注意此处的yield
多了个*
此处的意思是执行数组迭代如果没有*
则直接返回一个那个数组了([call(output,0),call(output,1)]
)。js中数组和其他的一些数据结构是可迭代的,所以当yield*
则执行了其内部迭代。
执行此上述代码,我们发现每隔2s打印一个值,它成为同步阻塞了!原因是将此数组放入了一个迭代环境,及数组中的每个元素都是yield
返回的,而saga
中对promise
是将下次迭代放入到回调then
中,所以当then
执行时才能执行下次迭代,处理数组中下一个元素。
一些saga
的初用者或是async/await
的初用者,都容易将代码这样写:
yield promise1()
yield promise2()
或是
await promise1()
await promise2()
promise1
和promise2
两者并没有依赖性,错误的使用使得此代码同步阻塞,ui等待数据的时间变长,用户的体验也会大打折扣。
这两种语法使得代码看起来更可读,且更直观,但带来的也是更容易忽略这种无意间造成同步阻塞的情况。
call方法源码及功能解析
直接去effectRunnerMap.js
看runCallEffect
方法
runCallEffect方法功能及源码解析
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)
}
}
接着上面的示例代码,我们的output
的方法作为call
方法的第一参数。而在runCallEffect
方法中,解构的fn
既是我们的output
。
首先const result=fn.apply(context, args)
执行我们的output
方法result
保存返回值。
然后对返回结构进行判断,是否是promise
,是否是迭代器,以上两者都不是直接执行回调cb
。
看第一种情况上述我们将output改成返回promise的方法,所以执行的是对promise
的处理逻辑,同样resolvePromise
执行对下次迭代的回调函数cb
(应该是chCbAtKey
方法)放入到then
回调中。digestEffect
此时执行完毕。
第二种情况当返回的是一个迭代器,迭代结束后返回。
proc方法内部细节:Task
上述第二种情况调用了proc
方法,我们之前在上一篇redux-saga学习笔记只讲述了proc
对于迭代器迭代后结果处理的几种方式,但如果迭代器迭代结束之后会做哪些事情?
redux-saga学习笔记忽略了saga
中一个重要概念Task
。
将示例代码output
方法改变一下让它成为一个生成器函数,其它的不动
function *output(count){
yield 'output开始迭代'
yield count
}
回到proc
方法内部代码中:
//proc方法代码片段
if (!result.done) {
digestEffect(result.value, parentEffectId, next)
} else {
/**
This Generator has ended, terminate the main task and notify the fork queue
**/
if (mainTask.status !== CANCELLED) {
mainTask.status = DONE
}
mainTask.cont(result.value)
}
可以看到当迭代未结束则调用digestEffect
处理,迭代结束后则执行else段的代码。
注意这段代码mainTask.cout(result.value)
,我们首先关注这个mainTask
它是怎么来的。
/** 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)
我们发现mainTask
刚开始仅仅是个简单的标记对象,然后作为第二参数传给newTask
方法
newTask方法源码
源码大致看一下就可以
function newTask(env, mainTask, parentContext, parentEffectId, meta, isRoot, cont = noop) {
let status = RUNNING
let taskResult
let taskError
let deferredEnd = null
const cancelledDueToErrorTasks = []
const context = Object.create(parentContext)
const queue = forkQueue(
mainTask,
function onAbort() {
cancelledDueToErrorTasks.push(...queue.getTasks().map(t => t.meta.name))
},
end,
)
function cancel() {
if (status === RUNNING) {
// Setting status to CANCELLED does not necessarily mean that the task/iterators are stopped
// effects in the iterator's finally block will still be executed
status = CANCELLED
queue.cancelAll()
// Ending with a TASK_CANCEL will propagate the Cancellation to all joiners
end(TASK_CANCEL, false)
}
}
function end(result, isErr) {
if (!isErr) {
// The status here may be RUNNING or CANCELLED
// If the status is CANCELLED, then we do not need to change it here
if (result === TASK_CANCEL) {
status = CANCELLED
} else if (status !== CANCELLED) {
status = DONE
}
taskResult = result
deferredEnd && deferredEnd.resolve(result)
} else {
status = ABORTED
sagaError.addSagaFrame({ meta, cancelledTasks: cancelledDueToErrorTasks })
if (task.isRoot) {
const sagaStack = sagaError.toString()
// we've dumped the saga stack to string and are passing it to user's code
// we know that it won't be needed anymore and we need to clear it
sagaError.clear()
env.onError(result, { sagaStack })
}
taskError = result
deferredEnd && deferredEnd.reject(result)
}
task.cont(result, isErr)
task.joiners.forEach(joiner => {
joiner.cb(result, isErr)
})
task.joiners = null
}
function setContext(props) {
if (process.env.NODE_ENV !== 'production') {
check(props, is.object, createSetContextWarning('task', props))
}
assignWithSymbols(context, props)
}
function toPromise() {
if (deferredEnd) {
return deferredEnd.promise
}
deferredEnd = deferred()
if (status === ABORTED) {
deferredEnd.reject(taskError)
} else if (status !== RUNNING) {
deferredEnd.resolve(taskResult)
}
return deferredEnd.promise
}
const task = {
// fields
[TASK]: true,
id: parentEffectId,
meta,
isRoot,
context,
joiners: [],
queue,
// methods
cancel,
cont,
end,
setContext,
toPromise,
isRunning: () => status === RUNNING,
isCancelled: () => status === CANCELLED || (status === RUNNING && mainTask.status === CANCELLED),
isAborted: () => status === ABORTED,
result: () => taskResult,
error: () => taskError,
}
return task
}
我们着重看下对mainTask
处理的地方这段代码。
const queue = forkQueue(
mainTask,
function onAbort() {
cancelledDueToErrorTasks.push(...queue.getTasks().map(t => t.meta.name))
},
end,
)
我们看看forkQueue
方法对mainTask
处理。forkQueue
代码片段
function forkQueue(mainTask, onAbort, cont/*end*/) {
addTask(mainTask)
function addTask(task/*mainTask*/) {
tasks.push(task)
task.cont = (res, isErr) => {
task.cont = noop
if (isErr) {
abort(res)
} else {
if (task === mainTask) {
result = res
}
if (!tasks.length) {
completed = true
cont(result)
}
}
}
}
}
这里提下这段代码 task.cont = noop
当mainTask.cout
方法执行时,会将其重新赋值为noop
,看下noop
的代码:
let noop = () => {}
if (process.env.NODE_ENV !== 'production' && typeof Proxy !== 'undefined') {
noop = new Proxy(noop, {
set: () => {
throw internalErr('There was an attempt to assign a property to internal `noop` function.')
},
})
}
由此可见noop
只是个什么都不做的空函数,但此处有个细节就是对noop
做了个proxy
此proxy
将阻止对noop
进行任何设置,以防止无意间的错误行为。
看这段代码cont(result)
cont
方法是newTask
方法内部的end
方法
end方法片段:
function end(result, isErr) {
task.cont(result, isErr)
task.joiners.forEach(joiner => {
joiner.cb(result, isErr)
})
task.joiners = null
}
这里task.cont
是由runCallEffect
方法内proc(env, result, task.context, currentEffectId, getMetaInfo(fn), /* isRoot */ false, cb)
这段代码传入的cb
而来。
而cb
则由all
方法内部
keys.forEach(key => {
digestEffect(effects[key]/*call(output)*/, effectId, childCallbacks[key], key)
})
childCallbacks[key]
传入根据之前all
方法功能解析得知为chCbAtKey
。
到此总结下runCallEffect
处理迭代器的过程,每当执行proc
方法时,内部都会创建个Task
,此task
的cont
属性会保存来自闭包了调用它的外部迭代器的next
方法,当然也不全是next
方法,像在all
中则是chCbAtKey
,但chCbAtKey
内部仍然调用了next
,归根结底还是同样。
all
方法它的作用是将执行结果保存起来当其内部传给它的所有元素都执行结束后,再继续执行迭代,要做到这点就必须存在chCbAtKey
这种方法将结果保存起来,等都执行结束后调用其内部代码:
function checkEnd() {
if (completedCount === totalCount) {
completed = true
console.log(results,'childCallbacks')
parentCallback(results/*收集的返回结果集合*/)//继续迭代执行
}
}
我们改变一下示例代码:
function* rootSaga() {
-const [a,b]= yield all([call(output,0),call(output,1)])
+const a=yield call(output,0)
console.error('测试call',a)
}
然后再将mainTask.cont(result.value)
这段代码注释掉,我们发现console.error
这段代码不执行了,可以得出一个结论,call
方法接管了后续的迭代。当call
执行完方法也就是用户传给它的生成器函数迭代器迭代完,它会接着走这段代码mainTask.cont(result.value)
,若mainTask.cont(result.value)
被注释掉,则后续的rootSaga
生成器函数的迭代无法执行。
我们也可以想下当传给call
的是一个异步方法时,call
对异步的处理方式是将下次迭代的回调方法放入异步then
回调中。call
方法接管了外部迭代回调方法
我们得到一个结论call
是一个同步阻塞的方法
这里头task
很有意思,它描述了每当proc
时其内部的一些信息,并存在一些方法,这些方法用来执行相应逻辑,做些善后工作,很有操作系统中Task
的感觉。
此时我们会想如果我们不想同步阻塞,那应该用什么方法?答案是fork
fork方法源码及功能解析
我们知道call
之所以会阻塞是因为它接管了接下来的迭代操作,尤其当call
的是一个异步方法时,会将迭代操作放到此异步方法的then
回调中,也就是说如果then
在10秒钟之后才执行,那么我们需要等待10秒call
方法后面的代码才能执行。很多时候并不需要等待回调,我们可以直接继续执行接下来的代码。
所以我们需要一种类似于call
方法,它像call
一样能执行我们自定义的方法,但又不像call
那样接管接下来的一切。那如何做到这一点呢,我们知道fork
主要功能在effectRunnerMap.js
中runForkEffect
中,那我们就想象下runForkEffect
方法中应该执行什么吧。
function runForkEffect(fn,cb){
fn()
cb()
}
runForkEffect
一定会收到这两个参数一个是我们自定义的方法,和接下来的迭代回调,为了避免阻塞的出现那么就会是上述想象代码的方式,而call的方式就会是这样:
fn().then(()=>{cb()})
我们必须等待异步回调才能执行接下来的代码。
让我们看看真实的runForkEffect
方法。
runForkEffect方法源码及功能解析
runForkEffect
源码:
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
}
在runForkEffect
中,可以看到想把任何传过来的fn
,都让它返回一个迭代器,createTaskIterator
对于普通函数的处理是直接让返回的迭代器在被迭代时,返回一个表示迭代完的结果。而对于返回promise
类型的方法则表示没有迭代完。对于生成器函数直接返回执行它所得到的迭代器就可以了。
对于非生成器函数,存在一个next
方法来作为其迭代所执行的方法。
// result为执行fn返回的结果
const next = arg => {
if (!resolved) {
resolved = true
// Only promises returned from fork will be interpreted. See #1573
return { value: result, done: !is.promise(result) }
} else {
return { value: arg, done: true }
}
}
可以清晰看到处理逻辑,因为只有promise
会造成阻塞,所以我们推理一下如果fn
返回的是一个promise
接下来会如何执行。
将createTaskIterator
方法返回的迭代器作为参数传入到proc
方法中,我们注意一下:runCallEffect
调用proc
方法和这里调用proc
方法传参,有些不同,这里的proc
方法最后一个参数为undefined
,而runCallEffect
中则是下次迭代的回调方法。
所以看出fork
不阻塞的原因了么?我们将runForkEffect
方法简单化一下。
function runForkEffect(env, { context, fn , args, detached }, cb, { task: parent }) {
const taskIterator = createTaskIterator({ context, fn, args })
const child = proc(env, taskIterator, parent.context, currentEffectId, meta, detached, undefined)
cb(child)
}
而前两行代码就已经把我们的自定义的逻辑执行了,在proc
内部runEffect
方法中有对promise
的处理,会将接下来相关的迭代,放到promise
的then
回调中,然后proc
方法就执行完毕了,接下来就执行cb
了,也就是我们其他代码逻辑。
我们再看下runForkEffect
源码我们发现此方法没有返回值,那也就证明不能这样的写法了const a= yield fork(output,0)
原因是我们什么也不会得到。问题来了,我们如何处理异步回来的数据?
最好也是最符合saga
的方式便是将我们的异步方法放入到一个我们自定义的生成器函数内部。
function output(count){
const promise=new Promise((resolve)=>{
setTimeout(()=>{
resolve(count)
},2000)
})
return promise
}
function *asyncWrapper(){
const a= yield call(output,0)
console.log('获取异步返回的值',a)
}
function *rootSaga(){
yield fork(asyncWrapper)
console.log('看看是否阻塞')
}
执行后我们发现看看是否阻塞
先打印出来,等待2s后获取异步返回的值
打印出来,所以得出结论,fork执行异步时,异步只会阻塞其相关逻辑这是我们所期望的,其他代码会立即执行。
fork源码以及功能解析结束。
takeEvery功能解析
与以往不同我们不直接看effectRunnerMap.js
,去看它的定义。
function takeEvery(patternOrChannel, worker, ...args) {
return fork(takeEveryHelper, patternOrChannel, worker, ...args)
}
在这里我们看到了上述讲述fork
方法,所以我们可以知道takeEvery方法是不阻塞的。
fork
方法要执行的方法为takeEveryHelper
,后面patternOrChannel, worker, ...args
为传给takeEveryHelper
的参数。其中patternOrChannel
为我们要监听的action.type
。
所以我们要知道这个takeEveryHelper
是什么?
sagaHelper中takeEvery源码
让我们看看takeEveryHelper
的定义。
function takeEvery(patternOrChannel /* action.type */, worker /* 自定义生成器 */, ...args) {
const yTake = { done: false, value: take(patternOrChannel) /* {@@redux-saga/IO: true, combinator: false, type: 'TAKE', payload:{ pattern: patternOrChannel } } */ }
const yFork = ac => ({ done: false, value: fork(worker, ...args, ac) })
let action,
setAction = ac => (action = ac)
return fsmIterator(
{
q1() {
return { nextState: 'q2', effect: yTake, stateUpdater: setAction }
},
q2() {
return { nextState: 'q1', effect: yFork(action) }
},
},
'q1',
`takeEvery(${safeName(patternOrChannel)}, ${worker.name})`,
)
}
首先我们看一下yTake,yFork
,这两个分别调用了take
和fork
。take
的功能是注册对action.type
的监听,而一旦相应的action.type
被dispatch
发出,fork
就会执行。
我们可以看出takeEvery
这个effect
方法,它的功能是take
和fork
的结合。take
注册监听,fork
响应监听。
我们直接用take
和fork
可以代替takeEvery
么?恐怕不行。
原因是take
只会监听一次我们所传入的type
,如果想让它始终监听有效,就要在响应后再次调用take
注册
之所以只监听一次是在channel.put
时会调用cb.cancel
,将cb从任务栈清除,而take
注册的方式是将cb
存入任务栈。
这么做的原因是担心无意间的take
存入太多重复的逻辑吧。
此处逻辑代码:
//在multicastChannel.put方法中
const takers = (currentTakers = nextTakers)//cb 为next包装存到下一个任务中
for (let i = 0, len = takers.length; i < len; i++) {
const taker = takers[i]
if (taker[MATCH](input)) {
taker.cancel()
taker(input)
}
}
}
//cancel的定义在multicastChannel.take方法中
cb[MATCH] = matcher
ensureCanMutateNextTakers()
nextTakers.push(cb)
cb.cancel = once(() => {
ensureCanMutateNextTakers()
remove(nextTakers, cb)
})
所以我们需要一处逻辑当我们注册的监听被响应后,应该再重新注册下。
注意此处代码:
{
q1() {
return { nextState: 'q2', effect: yTake, stateUpdater: setAction }
},
q2() {
return { nextState: 'q1', effect: yFork(action) }
},
}
我们注意返回的对象中nextState
的值,这也就说明q1
的下一个执行的是q2
,q2
下一个执行的是q1
这也形成了一种无限循环。当q1
开启对action.type
的监听,等到用户dispatch
对应的action
时,开始执行q2
逻辑,在执行q2
逻辑之前channel.put
把之前的监听取消掉。q2
执行后又执行q1
,再次开启监听。
这里我们重点需要关注的是take
方法。
take方法功能解析
take
方法定义在io.js
中,内部依旧是使用makeEffect
方法返回一个对其功能描述的对象,所以take
到底做了什么还是要看在effectRunnerMap.js
中runTakeEffect
方法。
runTakeEffect方法源码
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) /* 自定义saga的pattern会与接下来dispatch的action.type匹配 */ : null,pattern)
} catch (err) {
cb(err, true)
return
}
cb.cancel = takeCb.cancel
}
这里我们主要看这行代码channel.take(takeCb, is.notUndef(pattern) ? matcher(pattern):null, ,pattern)
。
matcher
方法:
export const array = patterns => input => patterns.some(p => matcher(p)(input))
export const predicate = predicate => input => predicate(input)
export const string = pattern => input => input.type === String(pattern)
export const symbol = pattern => input => input.type === pattern
export const wildcard = () => kTrue
export default function matcher(pattern) {
// prettier-ignore
const matcherCreator = (
pattern === '*' ? wildcard
: is.string(pattern) ? string
: is.array(pattern) ? array
: is.stringableFunc(pattern) ? string
: is.func(pattern) ? predicate
: is.symbol(pattern) ? symbol
: null
)
if (matcherCreator === null) {
throw new Error(`invalid pattern: ${pattern}`)
}
return matcherCreator(pattern)
}
实际上返回了一个闭包了传入的参数的方法,此方法的返回值为传入的参数和闭包的参数的比对结果,一个布尔值。它包含了对一些类型的判断逻辑。
功能体现上也就是matcher('a')('a')
是true
,matcher('a')('b')
是false
。
channel.take方法
来看看channel.take方法:
//channel对象的take方法
{ ...
take(cb, matcher = matchers.wildcard,pattern) {
if (process.env.NODE_ENV !== 'production') {
checkForbiddenStates()
}
if (closed) {
cb(END)
return
}
cb[MATCH] = matcher
cb.describe=pattern
ensureCanMutateNextTakers()
nextTakers.push(cb)
cb.cancel = once(() => {
ensureCanMutateNextTakers()
remove(nextTakers, cb)
})
}
}
可以看到如果matcher
没有传默认就是匹配一切(wildcard
:通用匹配符)
现将匹配函数存入到cb[MATCH]
,然后nextTakers.push(cb)
将cb
存入到nextTakers
,定义一个取消任务的方法存入到cb.cancel
中,用来取消监听。
我们响应监听是调用了channel.put
方法。这里简要说下put
的逻辑,将上述的nextTakers
进行遍历,每次遍历获取其存储的各个cb
,并与传入的action.type
比对,比对成功则先调用cb.cancel
取消监听然后cb
执行。
一般而言让proc执行完毕出调用栈的几种情况分别是:
迭代器彻底迭代完了,
执行到异步逻辑等待then
回调。
处理take
类型的effect
。
可能会疑惑为什么处理take
就会proc
执行完毕出栈呢,原因很简单因为下次的迭代的回调没有执行而被暂时保存起来了,这就是take
方法的主要功能。它只是拉开了弓弦,并等待发射。
takeLatest源码及功能解析
与takeEvery
不同的一点是,takeEvery
在处理异步时,如果用户多次触发,就会多次执行异步后的逻辑,而takeLatest
之后执行最后的那个异步回调。最直接的提现就是在saga源码项目example内的counter示例中,takeEvery
多次点击按钮,显示的clicked
计数就会增加多次,但如果用takeLastest
也就只增加一次。
结论是,takeLastest
只会处理最新的结果,之前的都会被取消掉。
takeLatest
定义:
function takeLatest(patternOrChannel, worker, ...args) {
return fork(takeLatestHelper, patternOrChannel, worker, ...args)
}
我们需注意takeLatestHelper
方法,因为与takeEvery
的不同就存在这里。
takeLatestHelper
方法的定义:
function takeLatest(patternOrChannel, worker, ...args) {
const yTake = { done: false, value: take(patternOrChannel) }
const yFork = ac => ({ done: false, value: fork(worker, ...args, ac) })
const yCancel = task => ({ done: false, value: cancel(task) })
let task, action
const setTask = t => (task = t)
const setAction = ac => (action = ac)
return fsmIterator(
{
q1() {
return { nextState: 'q2', effect: yTake, stateUpdater: setAction }
},
q2() {
return task
? { nextState: 'q3', effect: yCancel(task) }
: { nextState: 'q1', effect: yFork(action), stateUpdater: setTask }
},
q3() {
return { nextState: 'q1', effect: yFork(action), stateUpdater: setTask }
},
},
'q1',
`takeLatest(${safeName(patternOrChannel)}, ${worker.name})`,
)
}
与takeEvery
不同是多了个q3
状态,尤其q2
有了个判断,判断任务是否已经存在,若存在则取消任务,取消任务后下次状态设为q1
再次开启新任务。
那么取消任务的逻辑的是什么?
我们首先得知道任务从哪里传进来,在上面takeLastest
源码中我们知道有个将设置Task
的方法setTask
方法赋值给stateUpdate
,谁又调用stateUpdate
呢?
fsmIterator
创造一个迭代器,而执行迭代器的next
方法才会继续迭代,我们看下fsmIterator
中next
的代码片段:
function next(arg, error) {
stateUpdater && stateUpdater(arg)
const currentState = error ? fsm[errorState](error) : fsm[nextState]()
;({ nextState, effect, stateUpdater, errorState } = currentState)
return nextState === qEnd ? done(arg) : effect
}
可以看到task
由调用next
传入(arg),我们回到takeLastest
代码中,在q2
状态中当没有任务是调用fork
,而fork
调用proc
得到返回的任务(const child = proc(env, taskIterator, parent.context, currentEffectId, meta, detached, undefined)
)接着cb(child)
,而cb
既是fsmIterator
中的next
方法。
可能对为什么cb
会是fsmIterator
中的next
方法感到疑惑,我们只需要知道,,当一个迭代器执行next
,开始迭代,当执行到fork
时,会把这个能让这个迭代器的继续迭代的next
方法,传入到fork
中。我们知道执行q1
,q2
,q3
的迭代器是saga
模拟的,也就是当让这个迭代器迭代时,就会执行它的next
方法,也就是fsmIterator
中的next
方法。
回到我们本来的目的任务如何取消?看下const yCancel = task => ({ done: false, value: cancel(task) })
这段代码,看看cancel方法的定义。
cancel方法定义
//在io.js中
function cancel(taskOrTasks = SELF_CANCELLATION) {
if (process.env.NODE_ENV !== 'production') {
if (arguments.length > 1) {
throw new Error(
'cancel(...tasks) is not supported any more. Please use cancel([...tasks]) to cancel multiple tasks.',
)
}
if (is.array(taskOrTasks)) {
taskOrTasks.forEach(t => {
check(t, is.task, `cancel([...tasks]): argument ${t} is not a valid Task object ${TEST_HINT}`)
})
} else if (taskOrTasks !== SELF_CANCELLATION && is.notUndef(taskOrTasks)) {
check(taskOrTasks, is.task, `cancel(task): argument ${taskOrTasks} is not a valid Task object ${TEST_HINT}`)
}
}
return makeEffect(effectTypes.CANCEL, taskOrTasks)
}
同样是调用了makeEffect
,返回一个描述effect
对象。所以重点应该看看effectRunnerMap
中的runCancelEffect
方法
runCancelEffect源码及功能解析
话不多说看源码:
function runCancelEffect(env, taskOrTasks, cb, { task }) {
if (taskOrTasks === SELF_CANCELLATION) {
cancelSingleTask(task)
} else if (is.array(taskOrTasks)) {
taskOrTasks.forEach(cancelSingleTask)
} else {
cancelSingleTask(taskOrTasks)
}
cb()
// cancel effects are non cancellables
}
显而易见关键在cancelSingleTask
方法中。
cancelSingleTask方法源码
function cancelSingleTask(taskToCancel) {
if (taskToCancel.isRunning()) {
taskToCancel.cancel()
}
}
执行了任务本身的cancel()方法。继续看下去
task.cancel方法源码
function cancel() {
if (status === RUNNING) {
// Setting status to CANCELLED does not necessarily mean that the task/iterators are stopped
// effects in the iterator's finally block will still be executed
status = CANCELLED
queue.cancelAll()
// Ending with a TASK_CANCEL will propagate the Cancellation to all joiners
end(TASK_CANCEL, false)
}
}
将任务内status
设置为CANCELLED
,然后执行end
方法,其内部调用了cont
方法,cont
其实就是父迭代器下次的迭代的回调方法。整个取消过程就是取消接下来迭代,直接开始父迭代器迭代。
以迭代来看就是:
父迭代器迭代遇到生成器函数-->产出一个子迭代器-->子迭代器开始迭代-->子迭代器执行完-->父迭代器继续迭代。
以任务角度来看既是:
父任务开启子任务-->子任务开始执行-->子任务执行结束-->父任务继续执行。
它并不像外部方法调用内部方法,等内部方法执行完出栈,自动执行外部代码的剩余逻辑。
而是当子迭代器开始迭代时,也就是调用proc
方法,会创建一个task
,将父迭代器的迭代回调存入到task.cont
,待子迭代迭代完,将调用它。这个逻辑上述已经说过,但可以在这里再展示下:
//proc内部代码片段,当迭代没完成时执行digestEffect,当迭代结束时执行mainTask.cont(result.value)
if (!result.done) {
digestEffect(result.value, parentEffectId, next)
} else {
/**
This Generator has ended, terminate the main task and notify the fork queue
**/
if (mainTask.status !== CANCELLED) {
mainTask.status = DONE
}
mainTask.cont(result.value)
}
takeLeading源码及功能解析
function takeLeading(patternOrChannel, worker, ...args) {
if (process.env.NODE_ENV !== 'production') {
validateTakeEffect(takeLeading, patternOrChannel, worker)
}
return fork(takeLeadingHelper, patternOrChannel, worker, ...args)
}
同样重点在takeLeadingHelper
中
takeLeadingHelper源码及功能解析
function takeLeading(patternOrChannel, worker, ...args) {
const yTake = { done: false, value: take(patternOrChannel) }
const yCall = ac => ({ done: false, value: call(worker, ...args, ac) })
let action
const setAction = ac => (action = ac)
return fsmIterator(
{
q1() {
return { nextState: 'q2', effect: yTake, stateUpdater: setAction }
},
q2() {
return { nextState: 'q1', effect: yCall(action) }
},
},
'q1',
`takeLeading(${safeName(patternOrChannel)}, ${worker.name})`,
)
}
我们可以看到在结构上大致与takeEveryHelper
一样,只有q1
,q2
两个迭代状态,但细心可以发现,在q2
状态中effect
属性是yCall
而不是takeEveryHelper
中的yFork
。
yCall
中的主要功能体现就是call
,call
是同步阻塞的,也就意味着,当我们有个按钮,按钮的click
事件绑定着执行我们takeLeading
的方法,我们首次点击时,无论之后我们点击多少下,都会等到首次结果出来时才能执行。
如果对这里仍有疑惑,请参考上述takeEvery
,call
,fork
方法解析。
race方法及源码解析
race
方法,当内部执行队列中有率先完成时,整个执行就会结束,与all
不同all
是所有执行队列都完成,它才结束。
我们直接看effectRunnerMap.js
中runRaceEffect
方法
runRaceEffect方法源码及功能解析
function runRaceEffect(env, effects, cb, { digestEffect }) {
const effectId = currentEffectId
const keys = Object.keys(effects)
const response = is.array(effects) ? createEmptyArray(keys.length) : {}
const childCbs = {}
let completed = false
keys.forEach(key => {
const chCbAtKey = (res, isErr) => {
if (completed) {
return
}
if (isErr || shouldComplete(res)) {
// Race Auto cancellation
cb.cancel()
cb(res, isErr)
} else {
cb.cancel()
completed = true
response[key] = res
cb(response)
}
}
chCbAtKey.cancel = noop
childCbs[key] = chCbAtKey
})
cb.cancel = () => {
// prevents unnecessary cancellation
if (!completed) {
completed = true
keys.forEach(key => childCbs[key].cancel())
}
}
keys.forEach(key => {
if (completed) {
return
}
digestEffect(effects[key], effectId, childCbs[key], key)
})
}
其代码逻辑大致与all
方法相同,不同点在于race中每个方法执行结束后的回调方法chCbAtKey
,在
else {
cb.cancel()
completed = true
response[key] = res
cb(response)
}
当有一个方法执行完毕执行回调方法时进入此段逻辑,立即将complete
设置为true
,在all
中只有全部执行完毕才会设置。还有一个关键点cb.cancel()
cb.cancel = () => {
// prevents unnecessary cancellation
if (!completed) {
completed = true
keys.forEach(key => childCbs[key].cancel())
}
}
将任务队列的所有回调方法(childCbs[key]=chCbAtKey)
全部取消掉。
此取消操作做了些善后处理,经过观察基本都是执行noop
(()=>{}),并非像task.cancel
那样终止当前迭代,执行父迭代。
想要知道这其中更详细的逻辑,可以参考all
方法的解析
race
方法这一特点,我们可以较为轻松的设置一些任务超时,如文档示例所示
const {posts, timeout} = yield race({
posts : call(fetchApi, '/posts'),
timeout : call(delay, 1000)
})
设置一个1s的超时,如果请求未响应则终止。
有个更有意思的用法,如文档的第二示例
function* watchStartBackgroundTask() {
while(true) {
yield take('START_BACKGROUND_TASK')
yield race({
task: call(backgroundTask),
cancel: take('CANCEL_TASK')
})
}
}
take
方法在上述已经说过它会将迭代暂时挂起,等待用户dispatch
时响应,巧妙的地方就在这里,由于take
方法这种特性,它在race
环境中,它的回调必会是chCbAtKey
,但因为此回调被存入到channel
中的nextTask
,也就是被挂起了,它在未响应时不会执行。即便take
最先执行完,但因为它的回调没执行它不能取消任何race
中其他方法。
此时backgroundTask
在执行,假设backgroundTask
是一个同步阻塞的逻辑,它一直在执行。设想一下当我们dispatch
一个类型为CANCEL_TASK
的action
,此时take
存入的回调就会执行终止了backgroundTask
的执行。
要是不知道其原理,这个示例都看不懂。
put方法功能解析
如果使用saga,我们知道如果在saga
中派发一个action
到redux
去,就是使用put
方法(put(action)
)
put
定义在io
中,但其主要功能实现在runPutEffect
中
runPutEffect源码及功能解析
function runPutEffect(env, { channel, action, resolve }, cb) {
/**
Schedule the put in case another saga is holding a lock.
The put will be executed atomically. ie nested puts will execute after
this put has terminated.
**/
asap(() => {
let result
try {
//dispatch 会返回action
result = (channel ? channel.put : env.dispatch)(action)
//channel有可能是派发的action但被赋值给action这个参数了,这时channal被赋值为undefined
} catch (error) {
cb(error, true)
return
}
if (resolve && is.promise(result)) {
resolvePromise(result, cb)
} else {
cb(result)
}
})
// Put effects are non cancellables
}
我们注意此行代码result = (channel ? channel.put : env.dispatch)(action)
,如果channel
未定义则直接使用env.dispatch
方法,此方法便是由redux
传给中间件的dispatch
方法,如果想详细知道此过程,请直接看redux-saga学习笔记。
我们也看到put方法的另一功能就是调用channel.put
,此方法上述讲过,用来响应由channel.take
存入的
监听。要想用到这种功能可以在创造saga
中间件时传入一个我们自定义的channel
,然后用take
,和put
来操作channel
的响应和监听。
以上便是saga一些常用的effect功能解析,下一篇会继续对saga导出的其它一些方法进行剖析