react

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

ReduxSaga是目前為止,管理ReduxSideEffect最受歡迎的一個庫,其中基於Generator...

>

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 格式的方法。 需要注意的是, middleware 是先觸 發reducer(就是 next ),然後才呼叫 channel.put(action) , 也就是一個action發出,先觸發reducer,然後才觸發saga監聽 。 這裏我們先記住,當觸發一個 action ,這裏的 channel.put 就是 saga聽actio n的起點。
  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 方法中

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方法,整個流程大概是這樣的

img

只要 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 ,整個流程也就串起來了。

Facebook Profile photo
Written by Nat
This is the author box. A short description about the author of this article. Could be their website link, what they like to read about and so on. Profile