Redux-Saga源码解析(一) 初始化和take

Redux-Saga是目前为止,管理ReduxSideEffect最受欢迎的一个库,其中基于Generator的内部实现更是让人好奇,下面我会从入口开始,一步步剖析这其中神奇的地方。为了节省篇幅,下面代码中的源码部分作了大量精简,只保留主流程的代码。java

一. 初始化流程和take方法

修改官方Demo

咱们首先从官网fork一份Redux-Saga代码,而后在其中的examples/counter这个demo中开始咱们的源码之旅。按照文档中的介绍运行起来。 demo中用了takeEvery这个API,为了简单期见,咱们将takeEvery改成使用takegit

// counter/src/sagas/index.js

export default function* rootSaga() {
  while (true) {
    yield take('INCREMENT_ASYNC')
    yield incrementAsync()
  }
}
复制代码

初始化第一步:createSagaMiddleware

而后咱们回到counter/src/main.js 其中与saga有关的代码只有这些部分es6

import createSagaMiddleware from 'redux-saga'

import Counter from './components/Counter'
import reducer from './reducers'
import rootSaga from './sagas'

const sagaMiddleware = createSagaMiddleware()
const store = createStore(reducer, applyMiddleware(sagaMiddleware))
sagaMiddleware.run(rootSaga)
复制代码

其中createSagaMiddleware位于根目录的packages/core/src/internal/middleware.jsgithub

这里须要说起一下,Redux-SagaReact同样采用了monorepo的组织结构,也就是多仓库的结构。json

// packages/core/src/internal/middleware.js
// 为了简洁,删除了不少检查代码
export default function sagaMiddlewareFactory({ context = {}, channel = stdChannel(), sagaMonitor, ...options } = {}) {
  let boundRunSaga

  function sagaMiddleware({ getState, dispatch }) {
    boundRunSaga = runSaga.bind(null, {
      ...options,
      context,
      channel,
      dispatch,
      getState,
      sagaMonitor,
    })

    return next => action => {
      // 这里是dispatch函数
      if (sagaMonitor && sagaMonitor.actionDispatched) {
        sagaMonitor.actionDispatched(action)
      }
      // 从这里就能够看出来,先触发reducer,而后才再处理action,因此side effect慢于reducer
      const result = next(action) // hit reducers
      channel.put(action)
      return result
    }
  }

  sagaMiddleware.run = (...args) => {
    return boundRunSaga(...args)
  }

  sagaMiddleware.setContext = props => {
    assignWithSymbols(context, props)
  }

  // 这里本质上是标准redux middleware格式,即middlewareAPI => next => action => ...
  return sagaMiddleware
}
复制代码

createSagaMiddleware是构建sagaMiddleware的工厂函数,咱们在这个工厂函数里面须要注意3点:redux

  1. 注册middleware 真正给Redux使用的middleware就是内部的sagaMiddleware方法,sagaMiddleware最后也返回标准的Redux Middleware格式的方法,若是对Redux Middleware格式不了解能够看一下这篇文章。 须要注意的是,middleware是先触发reducer(就是next),而后才调用channel.put(action)也就是一个action发出,先触发reducer,而后才触发saga监听。 这里咱们先记住,当触发一个action,这里的channel.put就是saga听action的起点。
  2. 调用runSaga sagaMiddleware.run实际上就是runSaga方法
  3. channel参数 channel在这里看似是每次建立新的,但实际上整个saga只会在sagaMiddlewareFactory的参数中建立一次,后面会挂载在一个叫env的对象上重复使用,能够当作是一个单例理解。

初始化第二步: runSaga

下面简化后的runSaga函数数组

export function runSaga( { channel = stdChannel(), dispatch, getState, context = {}, sagaMonitor, effectMiddlewares, onError = logError },
  saga,
  ...args
) {
  // saga就是应用层的rootSaga,是一个generator
  // 返回一个iterator
  // 从这里能够发现,runSaga的时候能够传入更多参数,而后在saga函数中能够获取
  const iterator = saga(...args)

  const effectId = nextSagaId()

  let finalizeRunEffect
  if (effectMiddlewares) {
    const middleware = compose(...effectMiddlewares)
    finalizeRunEffect = runEffect => {
      return (effect, effectId, currCb) => {
        const plainRunEffect = eff => runEffect(eff, effectId, currCb)
        return middleware(plainRunEffect)(effect)
      }
    }
  } else {
    finalizeRunEffect = identity
  }

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

  return immediately(() => {
    const task = proc(env, iterator, context, effectId, getMetaInfo(saga), /* isRoot */ true, noop)

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

    return task
  })
}
复制代码

runSaga主要作了这几件事情bash

  1. 运行传入runSaga方法的rootSaga函数,保存返回的iterator
  2. 调用proc,并将上面rootSaga运行后返回的iterator传入proc方法中

此处要对Generator有必定了解, 建议阅读davidwalsh.name/es6-generat…系列,其中第二篇文章 我翻译了一下。app

proc方法

proc是整个saga运行的核心方法,笼统一点说,这个方法无非作了一件事,根据状况不停的调用iteratornext方法。也就是不断执行saga函数。异步

这时候咱们回到咱们的demo代码的saga部分。

import { put, take, delay } from 'redux-saga/effects'

export function* incrementAsync() {
  yield delay(1000)
  yield put({ type: 'INCREMENT' })
}

export default function* rootSaga() {
  while (true) {
    yield take('INCREMENT_ASYNC', incrementAsync)
  }
}
复制代码

当第一次调用next的时候,咱们调用了take方法,如今来看一下take方法作了些什么事情。

takeeffect相关的API在位置packages/core/src/internal/io.js,可是为了方便code splitingeffect部分代码在默认使用了packages/core/dist中已经被打包的代码。若是想在debug中运行到原来代码,须要将packages/core/effects.js中的package.json文件修改成未打包文件。具体能够参考git中的历史修改记录。

// take方法
export function take(patternOrChannel = '*', multicastPattern) {
  // 在咱们的demo代码中,只会走下面这个分支
  if (is.pattern(patternOrChannel)) {
    return makeEffect(effectTypes.TAKE, { pattern: patternOrChannel })
  }
  if (is.multicast(patternOrChannel) && is.notUndef(multicastPattern) && is.pattern(multicastPattern)) {
    return makeEffect(effectTypes.TAKE, { channel: patternOrChannel, pattern: multicastPattern })
  }
  if (is.channel(patternOrChannel)) {
    return makeEffect(effectTypes.TAKE, { channel: patternOrChannel })
  }
}
复制代码

当第一次执行take方法,咱们发现take方法只是简单的返回了一个由makeEffect制造的plain object

{
  "@@redux-saga/IO": true,
  "combinator": false,
  "type": "TAKE",
  "payload": {
    "pattern": "INCREMENT_ASYNC"
  }
}
复制代码

而后咱们回到proc方法,整个流程大概是这样的

proc方法流程图
只要 iterator.next().done不为 trueproc方法就会一直上面的流程。 digestEffectrunEffect是一些分支处理和回调的封装,在咱们目前的主流程能够先忽略,下面咱们以 take为例,看看 take是怎么监听 action

在next方法中执行了一次iterator.next()后,而后makeEffect获得take Effectplain object(咱们后面简称takeeffect)。而后在经过digestEffectrunEffect,运行runTakeEffect

// runTakeEffect
function runTakeEffect(env, { channel = env.channel, pattern, maybe }, cb) {
  const takeCb = input => {
    // 后面咱们会知道,这里的input就是action
    if (input instanceof Error) {
      cb(input, true)
      return
    }
    if (isEnd(input) && !maybe) {
      cb(TERMINATE)
      return
    }
    cb(input)
  }
  try {
    // 主要功能就是调用channel的take方法
    channel.take(takeCb, is.notUndef(pattern) ? matcher(pattern) : null)
  } catch (err) {
    cb(err, true)
    return
  }
  cb.cancel = takeCb.cancel
}
复制代码

这里的channel就是咱们新建sagaMiddleWare的channel,是multicastChannel的的返回值,位于packages/core/src/internal/channel.js 下面咱们看看multicastChannel的内容

export function multicastChannel() {
  let closed = false
  let currentTakers = []
  let nextTakers = currentTakers

  const ensureCanMutateNextTakers = () => {
    if (nextTakers !== currentTakers) {
      return
    }
    nextTakers = currentTakers.slice()
  }

  const close = () => {
    closed = true
    const takers = (currentTakers = nextTakers)
    nextTakers = []
    takers.forEach(taker => {
      taker(END)
    })
  }

  return {
    [MULTICAST]: true,
    put(input) {
      if (closed) {
        return
      }
      if (isEnd(input)) {
        close()
        return
      }
      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) {
      if (closed) {
        cb(END)
        return
      }
      cb[MATCH] = matcher
      ensureCanMutateNextTakers()
      nextTakers.push(cb)

      cb.cancel = once(() => {
        ensureCanMutateNextTakers()
        remove(nextTakers, cb)
      })
    },
    close,
  }
}
复制代码

能够看到multicastChannel返回的channel其实就三个方法,put,take,close,监听的action会被保存在nextTakers数组中,当这个take所监听的action被发出了,才会执行一遍next

到这里为止,咱们已经明白take方法的内部实现,take方法是用来暂停并等待执行action的一个side effect,那么接下来咱们来看看触发这样一个action的流程是怎样的。

二. action的触发

在demo的代码中,INCREMENT_ASYNC是经过saga监听的异步action。当咱们点击按钮increment async时,根据redux的middleware机制,action会在sagaMiddleware中被使用。咱们来看一下createSagaMiddleware的代码。

function sagaMiddleware({ getState, dispatch }) {
    // 省略其他部分代码
    return next => action => {
      // next是dispatch函数或者其余middleware
      // 从这里就能够看出来,先触发reducer,而后才再处理action,因此side effect慢于reducer
      const result = next(action) // hit reducers
      channel.put(action)
      return result
    }
  }
复制代码

能够看到,除了普通的middleware传递action, sagaMiddleware就只是调用了channel.put(action)。也就是咱们上文所说起的multicastChannelput方法。put方法会触发proc执行下一个next,整个流程也就串起来了。

总结

当执行runSaga以后,经过Generator中止-再执行的机制,会有一种在javaScript中另外开了一个线程的错觉,但实际上这也很像。另外Redux-Saga在流控制方面提供了更多的API,例如forkcallrace等,这些API对于组织复杂的action操做很是重要。深刻源码,除了能在工做中快速定位,也能加深在流操做方面的认识,这些API的源码解析会放在下一篇。

相关文章
相关标签/搜索