Redux-Saga
是目前爲止,管理Redux
的SideEffect
最受歡迎的一個庫,其中基於Generator
的內部實現更是讓人好奇,下面我會從入口開始,一步步剖析這其中神奇的地方。爲了節省篇幅,下面代碼中的源碼部分作了大量精簡,只保留主流程的代碼。java
咱們首先從官網fork一份Redux-Saga
代碼,而後在其中的examples/counter
這個demo中開始咱們的源碼之旅。按照文檔中的介紹運行起來。 demo中用了takeEvery
這個API,爲了簡單期見,咱們將takeEvery
改成使用take
。git
// counter/src/sagas/index.js
export default function* rootSaga() {
while (true) {
yield take('INCREMENT_ASYNC')
yield incrementAsync()
}
}
複製代碼
而後咱們回到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.js
,github
這裏須要說起一下,
Redux-Saga
和React
同樣採用了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
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
函數數組
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
runSaga
方法的rootSaga
函數,保存返回的iterator
proc
,並將上面rootSaga
運行後返回的iterator
傳入proc
方法中此處要對Generator有必定了解, 建議閱讀davidwalsh.name/es6-generat…系列,其中第二篇文章 我翻譯了一下。app
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
的流程是怎樣的。
在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的源碼解析會放在下一篇。