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的源碼解析會放在下一篇。

相關文章
相關標籤/搜索