Redux-Saga
是目前為止,管理Redux
的SideEffect
最受歡迎的一個庫,其中基於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-Saga
和React
一樣採用了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點:
- 註冊
middleware
真正給Redux
使用的middleware
就是內部的sagaMiddleware
方法,sagaMiddleware
最後也返回標準的Redux Middleware
格式的方法,如果對Redux Middleware
格式不瞭解可以看一下這篇文章。 需要注意的是,middleware
是先觸發reducer
(就是next
),然後才呼叫channel.put(action)
,也就是一個action發出,先觸發reducer,然後才觸發saga監聽。 這裡我們先記住,當觸發一個action
,這裡的channel.put
就是saga
監聽actio
n的起點。 - 呼叫
runSaga
sagaMiddleware.run實際上就是runSaga方法 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
主要做了這幾件事情
- 執行傳入
runSaga
方法的rootSaga
函式,儲存返回的iterator
- 呼叫
proc
,並將上面rootSaga
執行後返回的iterator
傳入proc
方法中
此處要對Generator有一定了解, 建議閱讀davidwalsh.name/es6-generat…系列,其中第二篇文章 我翻譯了一下。
proc方法
proc
是整個saga
執行的核心方法,籠統一點說,這個方法無非做了一件事,根據情況不停的呼叫iterator
的next
方法。也就是不斷執行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方法做了些什麼事情。
take
等effect
相關的API在位置packages/core/src/internal/io.js
,但是為了方便code spliting
,effect
部分程式碼在預設使用了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方法,整個流程大概是這樣的
只要iterator.next().done
不為true
,proc
方法就會一直上面的流程。
digestEffect
和runEffect
是一些分支處理和回撥的封裝,在我們目前的主流程可以先忽略,下面我們以take
為例,看看take
是怎麼監聽action
的
在next方法中執行了一次iterator.next()
後,然後makeEffect
得到take Effect
的plain object
(我們後面簡稱take
的effect
)。然後在通過digestEffect
和runEffect
,執行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)
。也就是我們上文所提及的multicastChannel
的put
方法。put
方法會觸發proc
執行下一個next
,整個流程也就串起來了。
總結
當執行runSaga
之後,通過Generator
的停止-再執行
的機制,會有一種在javaScript中另外開了一個執行緒的錯覺,但實際上這也很像。另外Redux-Saga
在流控制方面提供了更多的API,例如fork
、call
、race
等,這些API對於組織複雜的action操作非常重要。深入原始碼,除了能在工作中快速定位,也能加深在流操作方面的認識,這些API的原始碼解析會放在下一篇。