mominger / blog

Tech blog
45 stars 3 forks source link

Analysis of Redux-saga source code #38

Open mominger opened 2 years ago

mominger commented 2 years ago

The upper part is the English version, and the lower part is the Chinese version, with the same content. If there are any wrong, or you have anything hard to understand, pls feel free to let me know.many thx.

Overview

From The data flow, How to use, Debug the source code, The principle analysis of the core API, How to expand your own saga, and Further analyze the source code of key functions to analyze redux-saga in detail from 6 angles

1.The data flow

data-flow

2 How to use

  //register saga
  import { createStore, applyMiddleware } from 'redux';
  import createSagaMiddleware from 'redux-saga';
  import rootSaga from './sagas';

  const sagaMiddleware = createSagaMiddleware();
  const store = createStore(toDoApp, applyMiddleware(sagaMiddleware));
  sagaMiddleware.run(rootSaga);

  // every saga is a generator
  import { all, call, put, takeEvery } from 'redux-saga/effects';
  import { TODO_LIST } from '../actions';

  //request data, then go to dispatch action by put
  export function* fetchToDoList() {
    const data = yield call(fetch, url);
    yield put({ type: TODO_LIST, toDoList: data });
  }

  //Listen saga
  export function* loadToDoList() {
    yield takeEvery(LOAD_TODO_LIST, fetchToDoList);
  }

  //Listen to all saga in parallel
  export default function* rootSaga() {
    yield all([loadToDoList()]);
  }

Note: redux-saga does not intercept action, but captures action by listening. Because it uses channel to cache effect runner, and continues to execute next(). So the original action will still go to reducer

3 Debug the source code

3.1 How to debug the source code

  //1. Go into redux-saga/packages/redux-saga and execute the following command
  yarn link

  //2. Go into your project and execute the following command
  yarn link redux-saga

code_

Note: if you changed the source code, the corresponding code needs to be compiled. for example If the core source code is modified, redux-saga-effects.esm.js needs to be recompiled Or directly modify the code of node_module and debug by the chrome devtool breakpoint

3.2 Debug the execution flow of saga in chrome devtool

3.3.1 Entrance: store.js
import createSagaMiddleware from 'redux-saga';
import rootSaga from './sagas';

const sagaMiddleware = createSagaMiddleware();
const store = createStore(reducer, applyMiddleware(sagaMiddleware));
sagaMiddleware.run(rootSaga);

Use a simple saga project example to debug, you can use any project that contains saga, because the debugging process is the same

3.3.2 Key functions

createSagaMiddleware factory

sagaMiddleware.run run rootSaga rootSaga rootSaga_core

channel(stdChannel) channel

The two core methods of channel are take, put. take is for inserting the taker into the queue nextTakers, put is for iterating the queue, and execute the matching taker Taker is the next()

next next execute_runEffect0 fact_runEffect

If the generator function does not end, digestEffect will be executed, and finalRunEffect will be executed in digestEffect, and finalRunEffect actually executes the runEffect function

proc proc

Encapsulates next and runEffect functions, and executes next proc source code

runEffect runEffect

If it is an effect, get the corresponding runEffect from effectRunnerMap and execute it, if not, execute next() directly For example, effect.type is 'TAKE' or 'PUT', then execute runTakeEffect or runPutEffect

effectRunnerMap effectRunnerMap takeeffect putrunner

effectRunnerMap source code effectRunnerMap defines the runners corresponding to various side effects For example, when effect.type is 'TAKE', runTakeEffect will be executed, and if it is 'PUT', runPutEffect will be executed. Other Effects are similar The parameter cb in runTakeEffect and runPutEffect both refers to next. runTakeEffect is to stuff next function into the channel queue. runPutEffect is to execute store.dispatch.

3.3.3 Summarize

createSagaMiddleware(sagaMiddlewareFactory) rewrites the dispatch method, which executes channel.put. channel.put will iterate the taker (next function in proc.js) queue and execute the matching taker proc is executed in sagaMiddleware.run(rootSaga), next is executed in proc, and runEffect is executed in next. RunEffect judges that if it is an effect, execute the effectRunner matched by effectRunnerMap, if not, execute next. If the judgment is 'TAKE', execute runTakeEffect. The function of runTakeEffect is to put taker(next) back into the channel queue. Wait for the next UI component to call store.dispatch and trigger channel.put, iterate the channel queue, and execute the matched taker(next) The last next is the native store.dispatch The core function of next is to execute the generator down, which is why 'TAKE' blocks there, because it ends after putting next into the channel queue

4 Principle analysis of core API

core_api

4.1 Principle

source code

4.1.1 take

function runTakeEffect(env, {pattern}, cb) {
  ...
  env.channel.take(cb, matcher);
}

Stuff cb(next) into the channel queue It will be block, because cb(next) is not called after the queue is inserted, it means the generator is not continue to executed .

4.1.2 call

function runCallEffect(env, { fn, args }, cb) {
   const result = fn.apply(null, args);
    if (is.promise(result)) {
      return result.then(data => cb(data)).catch(error => cb(error, true));
    }
    cb(result);
  }

If call is a promise, it will block until Promise.resolve is invoked

4.1.2 put

function runPutEffect(env, { action }, cb) {
  const result = env.dispatch(action);
  cb(result);
}

Dispatch action to reducer It won't be block, because executed cb(next)

4.1.2 select

function runSelectEffect(env, { selector, args }, cb) {
   const state = selector(env.getState(), ...args)
   cb(state)
}

How to use

 let result = yield select(state => state.xxx);

 //or
 let result = yield select();

If there is no function passed in select, it will return all states by executing store.getState()

4.1.2 fork

function runForkEffect(env, { fn }, cb) {
  const taskIterator = createTaskIterator({ ...,fn})
  proc(env, taskIterator);
  cb();
}

It won't be block, because executed cb(next)

4.1.2 takeEvery & takeLastest

function takeEvery(pattern, saga) {
  function* takeEveryHelper() {
    while (true) {
      yield take(pattern);
      yield fork(saga);
    }
  }

  return fork(takeEveryHelper);
}

souce code Achieve the effect of each listening by an infinite loop generator pattern is action type takeEvery will trigger every time an action is listened, but takeLastest will only trigger the last time

5 How to extend your own saga

5.1 Add an effect runner

5.1.1 Adding types in effectTypes

type

5.1.2 Add the corresponding effect runner in effectRunnerMap

effect_add

6 Further analysis of the source code of key functions

redux-saga source code

6.1 execution flow

6.1.1 entrance in store
import createSagaMiddleware from 'redux-saga';

const sagaMiddleware = createSagaMiddleware();
sagaMiddleware.run(rootSaga);
6.1.2 Find the source code corresponding to the entry

The entrance in the core compiled file

6.1.3 sagaMiddlewareFactory

source code

export default function sagaMiddlewareFactory({ context = {}, channel = stdChannel(), sagaMonitor, ...options } = {}) {
    ...
    function sagaMiddleware({ getState, dispatch }) {
    // Provide redux-related parameters for runSaga
    boundRunSaga = runSaga.bind(null, {
      ...options,
      context,
      channel,
      dispatch,
      getState,
      sagaMonitor,
    })

    //returned middleware
    return next => action => {
      if (sagaMonitor && sagaMonitor.actionDispatched) {
        sagaMonitor.actionDispatched(action)
      }
      //excute next, then hit reducer
      const result = next(action) // hit reducers

      //add action in queue of channel
      channel.put(action)
      return result
    }
  }

  sagaMiddleware.run = (...args) => {
    if (process.env.NODE_ENV !== 'production' && !boundRunSaga) {
      throw new Error('Before running a Saga, you must mount the Saga middleware on the Store using applyMiddleware')
    }
    return boundRunSaga(...args)
  }

  sagaMiddleware.setContext = props => {
    if (process.env.NODE_ENV !== 'production') {
      check(props, is.object, createSetContextWarning('sagaMiddleware', props))
    }

    assignWithSymbols(context, props)
  }

  return sagaMiddleware
  ...
}
6.1.4 runSaga

source code

export function runSaga(
  //The first parameter is sent by redux  bind, and saga is the saga defined in the project
  { channel = stdChannel(), dispatch, getState, context = {}, sagaMonitor, effectMiddlewares, onError = logError },
  saga,
  ...args
) {
  if (process.env.NODE_ENV !== 'production') {
    check(saga, is.func, NON_GENERATOR_ERR)
  }

  // Execute the iterator returned by saga (generator function)
  const iterator = saga(...args)

  ...

  const env = {
    channel,
    dispatch: wrapSagaDispatch(dispatch),
    getState,
    sagaMonitor,
    onError,
    finalizeRunEffect,
  }

  // immediately is the scheduler.js task scheduling method, representing immediate execution
  return immediately(() => {
    //core methond: proc
    const task = proc(env, iterator, context, effectId, getMetaInfo(saga), /* isRoot */ true, undefined)

    if (sagaMonitor) {
      sagaMonitor.effectResolved(effectId, task)
    }

    return task
  })
}
6.1.5 channel

source code

 export function multicastChannel() {
    let closed = false

    //A task queue containing all callback functions (takers)
    let currentTakers = []
    let nextTakers = currentTakers

    ...

    return {
      //excute every taker
      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
        ...
        //add the cb(taker)
        nextTakers.push(cb)

        cb.cancel = once(() => {
          ensureCanMutateNextTakers()
          remove(nextTakers, cb)
        })
      },
      close,
    }
  }

  // stdChannel function is to execute  in outside
  export function stdChannel() {
    const chan = multicastChannel()
    const { put } = chan
    chan.put = input => {
      if (input[SAGA_ACTION]) {
        put(input)
        return
      }

      //asap is a scheduling function that only allows one task to execute at a time
      asap(() => {
        put(input)
      })
    }
    return chan
  }

The take of stdChannel is equivalent to listening, and put is equivalent to publishing Simple version

function stdChannel(){
let currentTakers = [];
function take(taker,matcher){ 
taker['MATCH']=matcher;
currentTakers.push(taker);
} 
function put(input){
for(let i=0;i<currentTakers.length;i++){
let taker = currentTakers[i]; 
let matcher = taker['MATCH']; 
if(matcher(input)){
taker(input);
}
}
return {take,put};
}
}
let channel = stdChannel()

6.1.6 proc

Execute the next of the Generator object iterator once, and execute the corresponding type of effect runner according to the result(effect type) source code

  export default function proc(env, iterator, parentContext, parentEffectId, meta, isRoot, cont) {
    ...
    *
    * receives either (command | effect result, false) or (any thrown thing, true)
    */
    function next(arg, isErr) {
      ...
      result = iterator.next(arg)
      ...
    }
    function runEffect(effect, effectId, 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)
      }
      ...
  }

Simple version

export default function proc(env,iteraton){
  function next(args){
    let result;
    //arg: genetor param //{done:false,value:{type:'TAKE'...}
    result = iterator.next(args);
    if(!result.done){
      runEffect(result.value,next);
    }
  }

  function runEffect(effect,next){ 
    if(effect){
     //different effect type to defferent effect runner
     const effectRunner =effectRunnerMap[effect.type];
     effectRunner(env,effect.payload,next);
    }else{
      next();
    }
  }
  next();
}

6.1.7 effectRunnerMap

source code

  ...
  //put effect: dispatch coresponding action
  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 {
        result = (channel ? channel.put : env.dispatch)(action)
      } catch (error) {
        cb(error, true)
        return
      }

      if (resolve && is.promise(result)) {
        resolvePromise(result, cb)
      } else {
        cb(result)
      }
    })
    // Put effects are non cancellables
  }
  ...
  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,
}

export default effectRunnerMap

different effect type to different effect runner

The following is the Chinese version, the same content as above

Overview

从数据流,如何使用,调试源码,核心API的原理分析,如何扩展自己的saga,对关键函数的源码进一步分析 6个角度来详细地剖析redux-saga

1. 数据流

data-flow

2 如何使用

  //注册saga
  import { createStore, applyMiddleware } from 'redux';
  import createSagaMiddleware from 'redux-saga';
  import rootSaga from './sagas';

  const sagaMiddleware = createSagaMiddleware();
  const store = createStore(toDoApp, applyMiddleware(sagaMiddleware));
  sagaMiddleware.run(rootSaga);

  // 每一个saga都是一个generator
  import { all, call, put, takeEvery } from 'redux-saga/effects';
  import { TODO_LIST } from '../actions';

  //请求数据,然后通过put去dispatch action
  export function* fetchToDoList() {
    const data = yield call(fetch, url);
    yield put({ type: TODO_LIST, toDoList: data });
  }

  //监听saga
  export function* loadToDoList() {
    yield takeEvery(LOAD_TODO_LIST, fetchToDoList);
  }

  //并行监听所有saga
  export default function* rootSaga() {
    yield all([loadToDoList()]);
  }

注意:redux-saga 并不是拦截action,而是通过监听去捕获action.因为它用channel缓存effect runner后,继续执行了next()。因此,原来的action仍会去redux

3 调试源码

3.1 如何调试源码

  //1. 进入  redux-saga/packages/redux-saga,执行以下命令
  yarn link

  //2. 进入项目,执行以下命令
  yarn link redux-saga

code_

注意,改了相应的源码,需要编译相应的代码。如修改了core源码,需重新编译 redux-saga-effects.esm.js 或直接修改 node_module的代码,在chrome devtool 打断点调试

3.2 在chrome devtool里调试saga的执行流程

3.3.1 入口: store.js

import createSagaMiddleware from 'redux-saga';
import rootSaga from './sagas';

const sagaMiddleware = createSagaMiddleware(); const store = createStore(reducer, applyMiddleware(sagaMiddleware)); sagaMiddleware.run(rootSaga);

> 以一个简单的[saga项目示例](https://github.com/mominger/react-todo)来调试,可以用包含saga的任意项目,因为调试流程都一样

##### 3.3.2 关键函数
createSagaMiddleware 
![factory](https://raw.githubusercontent.com/mominger/MyPublicFiles/master/img/20220319172701.png)

sagaMiddleware.run 
![run](https://raw.githubusercontent.com/mominger/MyPublicFiles/master/img/20220319172921.png)
rootSaga
![rootSaga](https://raw.githubusercontent.com/mominger/MyPublicFiles/master/img/20220319173119.png)
![rootSaga_core](https://raw.githubusercontent.com/mominger/MyPublicFiles/master/img/20220319173241.png)

channel(stdChannel)
![channel](https://raw.githubusercontent.com/mominger/MyPublicFiles/master/img/20220319194337.png)
> channel 两个核心方法 take、put. take 将taker塞入队列nextTakers,put则是迭代队列,执行匹配的taker
> taker 是 next()

next 
![next](https://raw.githubusercontent.com/mominger/MyPublicFiles/master/img/20220328135442.png)
![execute_runEffect0](https://raw.githubusercontent.com/mominger/MyPublicFiles/master/img/20220328135309.png)
![fact_runEffect](https://raw.githubusercontent.com/mominger/MyPublicFiles/master/img/20220328135713.png)
> 如果generator 函数没有结束,会执行digestEffect,digestEffect里面会执行finalRunEffect,finalRunEffect实际上执行的是runEffect函数

proc
![proc](https://raw.githubusercontent.com/mominger/MyPublicFiles/master/img/20220319201209.png)
> 封装了next 和 runEffect函数,并执行next  
> [源码](https://github.com/redux-saga/redux-saga/blob/master/packages/core/src/internal/proc.js)

runEffect
![runEffect](https://raw.githubusercontent.com/mominger/MyPublicFiles/master/img/20220328144030.png)
> 如果是effect,从effectRunnerMap里获取对应的runEffect,并执行,不是则直接执行next()
> 如effect.type为 'TAKE' 或 'PUT',执行runTakeEffect或runPutEffect

effectRunnerMap
![effectRunnerMap](https://raw.githubusercontent.com/mominger/MyPublicFiles/master/img/20220319201528.png)
![takeeffect](https://raw.githubusercontent.com/mominger/MyPublicFiles/master/img/20220319201645.png)
![putrunner](https://raw.githubusercontent.com/mominger/MyPublicFiles/master/img/20220319201910.png)
> [effectRunnerMap 源码](https://github.com/redux-saga/redux-saga/blob/master/packages/core/src/internal/effectRunnerMap.js)
> effectRunnerMap 定义了各类副作用对应的runner
> 如当effect.type为’TAKE'会执行 runTakeEffect,为‘PUT’则执行 runPutEffect.其他Effect类似
> runTakeEffect 和 runPutEffect 里的参数cb 都是指next。
> runTakeEffect是将next塞入channel队列。runPutEffect是执行store.dispatch.

##### 3.3.3 总结
> createSagaMiddleware(sagaMiddlewareFactory) 重写了dispatch方法,里面执行了channel.put. channel.put会迭代taker(proc.js里的next函数)队列,并执行匹配的taker
> sagaMiddleware.run(rootSaga)里执行了proc,proc里执行next,next里执行了runEffect. runEffect判断如果是effect,执行effectRunnerMap匹配到的effectRunner,如果不是,则执行next. 
> 如判定是'TAKE',则执行runTakeEffect. runTakeEffect的作用就是将taker(next)放回到channel队列里。等下一次ui组件调用store.dispatch又触发channel.put,迭代channel队列,执行匹配到的taker(next)
> 最后一个next是原生的store.dispatch
> next的核心作用就是将generator往下执行,这也是'TAKE'为何会阻塞在那,因为它将next放入channel队列后就结束了

### 4  核心API的原理分析
![core_api](https://raw.githubusercontent.com/mominger/MyPublicFiles/master/img/20220223184022.png)
#### 4.1 原理
> [源码](https://github.com/redux-saga/redux-saga/blob/master/packages/core/src/internal/effectRunnerMap.js)

#### 4.1.1 take

function runTakeEffect(env, {pattern}, cb) { ... env.channel.take(cb, matcher); }

> 将cb(next)塞入channel队列
> 会卡住,因为塞入队列后没调用cb(next),generator没往下执行了

#### 4.1.2 call

function runCallEffect(env, { fn, args }, cb) { const result = fn.apply(null, args); if (is.promise(result)) { return result.then(data => cb(data)).catch(error => cb(error, true)); } cb(result); }

> 如果call是promise,会阻塞调用
#### 4.1.2 put

function runPutEffect(env, { action }, cb) { const result = env.dispatch(action); cb(result); }

> dispatch action 到 reducer
> 不会卡住,因为继续执行了next

#### 4.1.2 select

function runSelectEffect(env, { selector, args }, cb) { const state = selector(env.getState(), ...args) cb(state) }

用法

let result = yield select(state => state.xxx);

//or let result = yield select();

> 如果select里没有传递函数,则执行store.getState(),返回所有的state
#### 4.1.2 fork

function runForkEffect(env, { fn }, cb) { const taskIterator = createTaskIterator({ ...,fn}) proc(env, taskIterator); cb(); }

>不会卡住,因为继续执行了next

#### 4.1.2 takeEvery & takeLastest

function takeEvery(pattern, saga) { function* takeEveryHelper() { while (true) { yield take(pattern); yield fork(saga); } }

return fork(takeEveryHelper); }

> [源码](https://github.com/redux-saga/redux-saga/blob/master/packages/core/src/internal/io-helpers.js)
> 通过无限循环的generator来达到每次监听的效果
> pattern 为action类型
> takeEvery 每次监听到action,都会触发,但takeLastest 只触发最后一次

> takeEvery 监听action,每监听到一个action,就执行一次操作
fork 异步非阻塞调用,无阻塞的执行fn,执行fn时,不会暂停Generator
一个 task 就像是一个在后台运行的进程,在基于redux-saga的应用程序中,可以同时运行多个task
>//监听每一个动作类型,当此动作发生的时候执行对应的worker
//takeEvery它回单开一个任务,并不会阻塞当前saga

### 5 如何扩展自己的saga
#### 5.1 新增一个effect runner
##### 5.1.1 在effectTypes里新增类型
![type](https://raw.githubusercontent.com/mominger/MyPublicFiles/master/img/20220319202729.png)
##### 5.1.2 在effectRunnerMap里增加对应的effect runner
![effect_add](https://raw.githubusercontent.com/mominger/MyPublicFiles/master/img/20220319205331.png)

### 6 对关键函数的源码进一步分析
[redux-saga源码](https://github.com/redux-saga/redux-saga)
#### 6.1 执行流程
#####  6.1.1  store 里的入口

import createSagaMiddleware from 'redux-saga';

const sagaMiddleware = createSagaMiddleware(); sagaMiddleware.run(rootSaga);


##### 6.1.2  找到入口对应的源码
core编译文件里的入口
![](https://raw.githubusercontent.com/mominger/MyPublicFiles/master/img/20220224153916.png)

![](https://raw.githubusercontent.com/mominger/MyPublicFiles/master/img/20220224154055.png)

![](https://raw.githubusercontent.com/mominger/MyPublicFiles/master/img/20220224153517.png)

##### 6.1.3  sagaMiddlewareFactory
[源码](https://github.com/redux-saga/redux-saga/blob/master/packages/core/src/internal/middleware.js)

export default function sagaMiddlewareFactory({ context = {}, channel = stdChannel(), sagaMonitor, ...options } = {}) { ... function sagaMiddleware({ getState, dispatch }) { // 为runSaga提供redux的相关参数 boundRunSaga = runSaga.bind(null, { ...options, context, channel, dispatch, getState, sagaMonitor, })

//返回的middleware
return next => action => {
  if (sagaMonitor && sagaMonitor.actionDispatched) {
    sagaMonitor.actionDispatched(action)
  }
  //先执行next,hit reducer
  const result = next(action) // hit reducers

  //加入channel
  channel.put(action)
  return result
}

}

sagaMiddleware.run = (...args) => { if (process.env.NODE_ENV !== 'production' && !boundRunSaga) { throw new Error('Before running a Saga, you must mount the Saga middleware on the Store using applyMiddleware') } return boundRunSaga(...args) }

sagaMiddleware.setContext = props => { if (process.env.NODE_ENV !== 'production') { check(props, is.object, createSetContextWarning('sagaMiddleware', props)) }

assignWithSymbols(context, props)

}

return sagaMiddleware ... }


##### 6.1.4  runSaga
[源码](https://github.com/redux-saga/redux-saga/blob/master/packages/core/src/internal/runSaga.js)

export function runSaga( //第一个参数是redux通过bind传来的,saga就是项目里定义的saga { channel = stdChannel(), dispatch, getState, context = {}, sagaMonitor, effectMiddlewares, onError = logError }, saga, ...args ) { if (process.env.NODE_ENV !== 'production') { check(saga, is.func, NON_GENERATOR_ERR) }

// 执行saga(generator函数)返回的迭代器 const iterator = saga(...args)

...

const env = { channel, dispatch: wrapSagaDispatch(dispatch), getState, sagaMonitor, onError, finalizeRunEffect, }

// immediately 是scheduler.js 任务调度方法,代表立即执行 return immediately(() => { //核心方法 proc const task = proc(env, iterator, context, effectId, getMetaInfo(saga), / isRoot / true, undefined)

if (sagaMonitor) {
  sagaMonitor.effectResolved(effectId, task)
}

return task

}) }


##### 6.1.5  channel
[源码](https://github.com/redux-saga/redux-saga/blob/master/packages/core/src/internal/channel.js)

export function multicastChannel() { let closed = false

//包含所有回调函数(taker)的任务队列
let currentTakers = []
let nextTakers = currentTakers

...

return {
  //执行每个回调函数
  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(() => {
      ensureCanMutateNextTakers()
      remove(nextTakers, cb)
    })
  },
  close,
}

}

//外界执行的 stdChannel 函数 export function stdChannel() { const chan = multicastChannel() const { put } = chan chan.put = input => { if (input[SAGA_ACTION]) { put(input) return }

  //asap 是一个调度函数,每次只允许一个任务执行
  asap(() => {
    put(input)
  })
}
return chan

}

> stdChannel 的take相当于监听 put相当于发布
简化写法

function stdChannel(){ let currentTakers = []; function take(taker,matcher){ taker['MATCH']=matcher; currentTakers.push(taker); } function put(input){ for(let i=0;i<currentTakers.length;i++){ let taker = currentTakers[i]; let matcher = taker['MATCH']; if(matcher(input)){ taker(input); } } return {take,put}; } } let channel = stdChannel()


#### 6.1.6 proc
执行一次Generator对象iterator的next,根据结果(effect type)执行对应类型的effect
[源码](https://github.com/redux-saga/redux-saga/blob/master/packages/core/src/internal/proc.js)

export default function proc(env, iterator, parentContext, parentEffectId, meta, isRoot, cont) { ... *

简化写法

export default function proc(env,iteraton){
  function next(args){
    let result;
    //arg: genetor param //{done:false,value:{type:'TAKE'...}
    result = iterator.next(args);
    if(!result.done){
      runEffect(result.value,next);
    }
  }

  function runEffect(effect,next){ 
    if(effect){
     //不同类型的effect走不同的执行流程
     const effectRunner =effectRunnerMap[effect.type];
     effectRunner(env,effect.payload,next);
    }else{
      next();
    }
  }
  next();
}

6.1.7 effectRunnerMap

源码

  ...
  //put effect: dispatch 对应的action
  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 {
        result = (channel ? channel.put : env.dispatch)(action)
      } catch (error) {
        cb(error, true)
        return
      }

      if (resolve && is.promise(result)) {
        resolvePromise(result, cb)
      } else {
        cb(result)
      }
    })
    // Put effects are non cancellables
  }
  ...
  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,
}

export default effectRunnerMap

不同的effect 对应不同的 effect runner