Redux-Saga原始碼解析(一) 初始化和take

雨棚發表於2019-04-10

Redux-Saga是目前為止,管理ReduxSideEffect最受歡迎的一個庫,其中基於Generator的內部實現更是讓人好奇,下面我會從入口開始,一步步剖析這其中神奇的地方。為了節省篇幅,下面程式碼中的原始碼部分做了大量精簡,只保留主流程的程式碼。

一. 初始化流程和take方法

修改官方Demo

我們首先從官網fork一份Redux-Saga程式碼,然後在其中的examples/counter這個demo中開始我們的原始碼之旅。按照文件中的介紹執行起來。 demo中用了takeEvery這個API,為了簡單期見,我們將takeEvery改為使用take

// counter/src/sagas/index.js

export default function* rootSaga() {
  while (true) {
    yield take('INCREMENT_ASYNC')
    yield incrementAsync()
  }
}
複製程式碼

初始化第一步:createSagaMiddleware

然後我們回到counter/src/main.js 其中與saga有關的程式碼只有這些部分

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.js

這裡需要提及一下,Redux-SagaReact一樣採用了monorepo的組織結構,也就是多倉庫的結構。

// 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點:

  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主要做了這幾件事情

  1. 執行傳入runSaga方法的rootSaga函式,儲存返回的iterator
  2. 呼叫proc,並將上面rootSaga執行後返回的iterator傳入proc方法中

此處要對Generator有一定了解, 建議閱讀davidwalsh.name/es6-generat…系列,其中第二篇文章 我翻譯了一下。

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

相關文章