redux-saga Effect方法功能以及源码解析

433 阅读21分钟

开始前最好看过我写过的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)
}

saga all方法执行过程.png

从源码入手

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,是否是迭代器,是否是sagaeffect,如果上述情况都不是即返回的就是一个值,随后将这个effect传入到next中。 终上所述,我们得知回调chCbAtKey方法第一参是怎么得到的,它是什么。
接着往下看看else里的逻辑,用results数组或是对象存储这个resresults最终会存储所有传入给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方法放到promisethen回调中。
由此可推测当将childCallbacks[key](chCbAtKey)放入到output返回的promisethen中,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()

promise1promise2两者并没有依赖性,错误的使用使得此代码同步阻塞,ui等待数据的时间变长,用户的体验也会大打折扣。 这两种语法使得代码看起来更可读,且更直观,但带来的也是更容易忽略这种无意间造成同步阻塞的情况。

call方法源码及功能解析

直接去effectRunnerMap.jsrunCallEffect方法

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 = noopmainTask.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做了个proxyproxy将阻止对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,此taskcont属性会保存来自闭包了调用它的外部迭代器的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.jsrunForkEffect中,那我们就想象下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的处理,会将接下来相关的迭代,放到promisethen回调中,然后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,这两个分别调用了takeforktake的功能是注册对action.type的监听,而一旦相应的action.typedispatch发出,fork就会执行。
我们可以看出takeEvery这个effect方法,它的功能是takefork的结合。take注册监听,fork响应监听。
我们直接用takefork可以代替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的下一个执行的是q2q2下一个执行的是q1 这也形成了一种无限循环。当q1开启对action.type的监听,等到用户dispatch对应的action时,开始执行q2逻辑,在执行q2逻辑之前channel.put把之前的监听取消掉。q2执行后又执行q1,再次开启监听。

这里我们重点需要关注的是take方法。

take方法功能解析

take方法定义在io.js中,内部依旧是使用makeEffect方法返回一个对其功能描述的对象,所以take到底做了什么还是要看在effectRunnerMap.jsrunTakeEffect方法。

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方法才会继续迭代,我们看下fsmIteratornext的代码片段:

  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的方法,我们首次点击时,无论之后我们点击多少下,都会等到首次结果出来时才能执行。 如果对这里仍有疑惑,请参考上述takeEverycall,fork方法解析。

race方法及源码解析

race方法,当内部执行队列中有率先完成时,整个执行就会结束,与all不同all是所有执行队列都完成,它才结束。
我们直接看effectRunnerMap.jsrunRaceEffect方法

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_TASKaction,此时take存入的回调就会执行终止了backgroundTask的执行。
要是不知道其原理,这个示例都看不懂。

put方法功能解析

如果使用saga,我们知道如果在saga中派发一个actionredux去,就是使用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导出的其它一些方法进行剖析