手寫Redux-Saga源碼
上一篇文章我們分析了Redux-Thunk的源碼,可以看到他的代碼非常簡單,只是讓dispatch可以處理函數(shù)類型的action,其作者也承認(rèn)對于復(fù)雜場景,Redux-Thunk并不適用,還推薦了Redux-Saga來處理復(fù)雜副作用。本文要講的就是Redux-Saga,這個(gè)也是我在實(shí)際工作中使用最多的Redux異步解決方案。Redux-Saga比Redux-Thunk復(fù)雜得多,而且他整個(gè)異步流程都使用Generator來處理,Generator也是我們這篇文章的前置知識(shí),如果你對Generator還不熟悉,可以看看這篇文章。
本文仍然是老套路,先來一個(gè)Redux-Saga的簡單例子,然后我們自己寫一個(gè)Redux-Saga來替代他,也就是源碼分析。
本文可運(yùn)行的代碼已經(jīng)上傳到GitHub,可以拿下來玩玩:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/React/redux-saga
簡單例子
網(wǎng)絡(luò)請求是我們經(jīng)常需要處理的異步操作,假設(shè)我們現(xiàn)在的一個(gè)簡單需求就是點(diǎn)擊一個(gè)按鈕去請求用戶的信息,大概長這樣:
這個(gè)需求使用Redux實(shí)現(xiàn)起來也很簡單,點(diǎn)擊按鈕的時(shí)候dispatch出一個(gè)action。這個(gè)action會(huì)觸發(fā)一個(gè)請求,請求返回的數(shù)據(jù)拿來顯示在頁面上就行:
import React from 'react';
import { connect } from 'react-redux';
function App(props) {
const { dispatch, userInfo } = props;
const getUserInfo = () => {
dispatch({ type: 'FETCH_USER_INFO' })
}
return (
<div className="App">
<button onClick={getUserInfo}>Get User Info</button>
<br></br>
{userInfo && JSON.stringify(userInfo)}
</div>
);
}
const matStateToProps = (state) => ({
userInfo: state.userInfo
})
export default connect(matStateToProps)(App);
上面這種寫法都是我們之前講Redux就介紹過的,Redux-Saga介入的地方是dispatch({ type: 'FETCH_USER_INFO' })之后。按照Redux一般的流程,FETCH_USER_INFO被發(fā)出后應(yīng)該進(jìn)入reducer處理,但是reducer都是同步代碼,并不適合發(fā)起網(wǎng)絡(luò)請求,所以我們可以使用Redux-Saga來捕獲FETCH_USER_INFO并處理。
Redux-Saga是一個(gè)Redux中間件,所以我們在createStore的時(shí)候?qū)⑺刖托校?/p>
// store.js
import { createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
import reducer from './reducer';
import rootSaga from './saga';
const sagaMiddleware = createSagaMiddleware()
let store = createStore(reducer, applyMiddleware(sagaMiddleware));
// 注意這里,sagaMiddleware作為中間件放入Redux后
// 還需要手動(dòng)啟動(dòng)他來運(yùn)行rootSaga
sagaMiddleware.run(rootSaga);
export default store;
注意上面代碼里的這一行:
sagaMiddleware.run(rootSaga);
sagaMiddleware.run是用來手動(dòng)啟動(dòng)rootSaga的,我們來看看rootSaga是怎么寫的:
import { call, put, takeLatest } from 'redux-saga/effects';
import { fetchUserInfoAPI } from './api';
function* fetchUserInfo() {
try {
const user = yield call(fetchUserInfoAPI);
yield put({ type: "FETCH_USER_SUCCEEDED", payload: user });
} catch (e) {
yield put({ type: "FETCH_USER_FAILED", payload: e.message });
}
}
function* rootSaga() {
yield takeEvery("FETCH_USER_INFO", fetchUserInfo);
}
export default rootSaga;
上面的代碼我們從export開始看吧,export的東西是rootSaga這個(gè)Generator函數(shù),這里面就一行:
yield takeEvery("FETCH_USER_INFO", fetchUserInfo);
這一行代碼用到了Redux-Saga的一個(gè)effect,也就是takeEvery,他的作用是監(jiān)聽每個(gè)FETCH_USER_INFO,當(dāng)FETCH_USER_INFO出現(xiàn)的時(shí)候,就調(diào)用fetchUserInfo函數(shù),注意這里是每個(gè)FETCH_USER_INFO。也就是說如果同時(shí)發(fā)出多個(gè)FETCH_USER_INFO,我們每個(gè)都會(huì)響應(yīng)并發(fā)起請求。類似的還有takeLatest,takeLatest從名字都可以看出來,是響應(yīng)最后一個(gè)請求,具體使用哪一個(gè),要看具體的需求。
然后看看fetchUserInfo函數(shù),這個(gè)函數(shù)也不復(fù)雜,就是調(diào)用一個(gè)API函數(shù)fetchUserInfoAPI去獲取數(shù)據(jù),注意我們這里函數(shù)調(diào)用并不是直接的fetchUserInfoAPI(),而是使用了Redux-Saga的call這個(gè)effect,這樣做可以讓我們寫單元測試變得更簡單,為什么會(huì)這樣,我們后面講源碼的時(shí)候再來仔細(xì)看看。獲取數(shù)據(jù)后,我們調(diào)用了put去發(fā)出FETCH_USER_SUCCEEDED這個(gè)action,這里的put類似于Redux里面的dispatch,也是用來發(fā)出action的。這樣我們的reducer就可以拿到FETCH_USER_SUCCEEDED進(jìn)行處理了,跟以前的reducer并沒有太大區(qū)別。
// reducer.js
const initState = {
userInfo: null,
error: ''
};
function reducer(state = initState, action) {
switch (action.type) {
case 'FETCH_USER_SUCCEEDED':
return { ...state, userInfo: action.payload };
case 'FETCH_USER_FAILED':
return { ...state, error: action.payload };
default:
return state;
}
}
export default reducer;
通過這個(gè)例子的代碼結(jié)構(gòu)我們可以看出:
action被分為了兩種,一種是觸發(fā)異步處理的,一種是普通的同步action。異步
action使用Redux-Saga來監(jiān)聽,監(jiān)聽的時(shí)候可以使用takeLatest或者takeEvery來處理并發(fā)的請求。具體的
saga實(shí)現(xiàn)可以使用Redux-Saga提供的方法,比如call,put之類的,可以讓單元測試更好寫。一個(gè)
action可以被Redux-Saga和Reducer同時(shí)響應(yīng),比如上面的FETCH_USER_INFO發(fā)出后我還想讓頁面轉(zhuǎn)個(gè)圈,可以直接在reducer里面加一個(gè)就行:... case 'FETCH_USER_INFO': return { ...state, isLoading: true }; ...
手寫源碼
通過上面這個(gè)例子,我們可以看出,Redux-Saga的運(yùn)行是通過這一行代碼來實(shí)現(xiàn)的:
sagaMiddleware.run(rootSaga);
整個(gè)Redux-Saga的運(yùn)行和原本的Redux并不沖突,Redux甚至都不知道他的存在,他們之間耦合很小,只在需要的時(shí)候通過put發(fā)出action來進(jìn)行通訊。所以我猜測,他應(yīng)該是自己實(shí)現(xiàn)了一套完全獨(dú)立的異步任務(wù)處理機(jī)制,下面我們從能感知到的API入手,一步一步來探尋下他源碼的奧秘吧。本文全部代碼參照官方源碼寫成,函數(shù)名字和變量名字盡量保持一致,寫到具體的方法的時(shí)候我也會(huì)貼出對應(yīng)的代碼地址,主要代碼都在這里:https://github.com/redux-saga/redux-saga/tree/master/packages/core/src
先來看看我們用到了哪些API,這些API就是我們今天手寫的目標(biāo):
- createSagaMiddleware:這個(gè)方法會(huì)返回一個(gè)中間件實(shí)例
sagaMiddleware- sagaMiddleware.run: 這個(gè)方法是真正運(yùn)行我們寫的
saga的入口- takeEvery:這個(gè)方法是用來控制并發(fā)流程的
- call:用來調(diào)用其他方法
- put:發(fā)出
action,用來和Redux通訊
從中間件入手
之前我們講Redux源碼的時(shí)候詳細(xì)分析了Redux中間件的原理和范式,一個(gè)中間件大概就長這個(gè)樣子:
function logger(store) {
return function(next) {
return function(action) {
console.group(action.type);
console.info('dispatching', action);
let result = next(action);
console.log('next state', store.getState());
console.groupEnd();
return result
}
}
}
這其實(shí)就相當(dāng)于一個(gè)Redux中間件的范式了:
- 一個(gè)中間件接收
store作為參數(shù),會(huì)返回一個(gè)函數(shù)- 返回的這個(gè)函數(shù)接收老的
dispatch函數(shù)作為參數(shù)(也就是上面的next),會(huì)返回一個(gè)新的函數(shù)- 返回的新函數(shù)就是新的
dispatch函數(shù),這個(gè)函數(shù)里面可以拿到外面兩層傳進(jìn)來的store和老dispatch函數(shù)
依照這個(gè)范式以及前面對createSagaMiddleware的使用,我們可以先寫出這個(gè)函數(shù)的骨架:
// sagaMiddlewareFactory其實(shí)就是我們外面使用的createSagaMiddleware
function sagaMiddlewareFactory() {
// 返回的是一個(gè)Redux中間件
// 需要符合他的范式
const sagaMiddleware = function (store) {
return function (next) {
return function (action) {
// 內(nèi)容先寫個(gè)空的
let result = next(action);
return result;
}
}
}
// sagaMiddleware上還有個(gè)run方法
// 是用來啟動(dòng)saga的
// 我們先留空吧
sagaMiddleware.run = () => { }
return sagaMiddleware;
}
export default sagaMiddlewareFactory;
梳理架構(gòu)
現(xiàn)在我們有了一個(gè)空的骨架,接下來該干啥呢?前面我們說過了,Redux-Saga很可能是自己實(shí)現(xiàn)了一套完全獨(dú)立的異步事件處理機(jī)制。這種異步事件處理機(jī)制需要一個(gè)處理中心來存儲(chǔ)事件和處理函數(shù),還需要一個(gè)方法來觸發(fā)隊(duì)列中的事件的執(zhí)行,再回看前面的使用的API,我們發(fā)現(xiàn)了兩個(gè)類似功能的API:
- takeEvery(action, callback):他接收的參數(shù)就是
action和callback,而且我們在根saga里面可能會(huì)多次調(diào)用它來注冊不同action的處理函數(shù),這其實(shí)就相當(dāng)于往處理中心里面塞入事件了。- put(action):
put的參數(shù)是action,他唯一的作用就是觸發(fā)對應(yīng)事件的回調(diào)運(yùn)行。
可以看到Redux-Saga這種機(jī)制也是用takeEvery先注冊回調(diào),然后使用put發(fā)出消息來觸發(fā)回調(diào)執(zhí)行,這其實(shí)跟我們其他文章多次提到的發(fā)布訂閱模式很像。
手寫channel
channel是Redux-Saga保存回調(diào)和觸發(fā)回調(diào)的地方,類似于發(fā)布訂閱模式,我們先來寫個(gè):
export function multicastChannel() {
const currentTakers = []; // 一個(gè)變量存儲(chǔ)我們所有注冊的事件和回調(diào)
// 保存事件和回調(diào)的函數(shù)
// Redux-Saga里面take接收回調(diào)cb和匹配方法matcher兩個(gè)參數(shù)
// 事實(shí)上take到的事件名稱也被封裝到了matcher里面
function take(cb, matcher) {
cb['MATCH'] = matcher;
currentTakers.push(cb);
}
function put(input) {
const takers = currentTakers;
for (let i = 0, len = takers.length; i < len; i++) {
const taker = takers[i]
// 這里的'MATCH'是上面take塞進(jìn)來的匹配方法
// 如果匹配上了就將回調(diào)拿出來執(zhí)行
if (taker['MATCH'](input)) {
taker(input);
}
}
}
return {
take,
put
}
}
上述代碼中有一個(gè)奇怪的點(diǎn),就是將matcher作為屬性放到了回調(diào)函數(shù)上,這么做的原因我想是為了讓外部可以自定義匹配方法,而不是簡單的事件名稱匹配,事實(shí)上Redux-Saga本身就支持好幾種匹配模式,包括字符串,Symbol,數(shù)組等等。
內(nèi)置支持的匹配方法可以看這里:https://github.com/redux-saga/redux-saga/blob/master/packages/core/src/internal/matcher.js。
channel對應(yīng)的源碼可以看這里:https://github.com/redux-saga/redux-saga/blob/master/packages/core/src/internal/channel.js#L153
有了channel之后,我們的中間件里面其實(shí)只要再干一件事情就行了,就是調(diào)用channel.put將接收的action再發(fā)給channel去執(zhí)行回調(diào)就行,所以我們加一行代碼:
// ... 省略前面代碼
const result = next(action);
channel.put(action); // 將收到的action也發(fā)給Redux-Saga
return result;
// ... 省略后面代碼
sagaMiddleware.run
前面的put是發(fā)出事件,執(zhí)行回調(diào),可是我們的回調(diào)還沒注冊呢,那注冊回調(diào)應(yīng)該在什么地方呢?看起來只有一個(gè)地方了,那就是sagaMiddleware.run。簡單來說,sagaMiddleware.run接收一個(gè)Generator作為參數(shù),然后執(zhí)行這個(gè)Generator,當(dāng)遇到take的時(shí)候就將它注冊到channel上面去。這里我們先實(shí)現(xiàn)take,takeEvery是在這個(gè)基礎(chǔ)上實(shí)現(xiàn)的。Redux-Saga中這塊代碼是單獨(dú)抽取了一個(gè)文件,我們仿照這種做法吧。
首先需要在中間件里面將Redux的getState和dispatch等參數(shù)傳遞進(jìn)去,Redux-Saga使用的是bind函數(shù),所以中間件方法改造如下:
function sagaMiddleware({ getState, dispatch }) {
// 將getState, dispatch通過bind傳給runSaga
boundRunSaga = runSaga.bind(null, {
channel,
dispatch,
getState,
})
return function (next) {
return function (action) {
const result = next(action);
channel.put(action);
return result;
}
}
}
然后sagaMiddleware.run就直接將boundRunSaga拿來運(yùn)行就行了:
sagaMiddleware.run = (...args) => {
boundRunSaga(...args)
}
注意這里的...args,這個(gè)其實(shí)就是我們傳進(jìn)去的rootSaga。到這里其實(shí)中間件部分就已經(jīng)完成了,后面的代碼就是具體的執(zhí)行過程了。
中間件對應(yīng)的源碼可以看這里:https://github.com/redux-saga/redux-saga/blob/master/packages/core/src/internal/middleware.js
runSaga
runSaga其實(shí)才是真正的sagaMiddleware.run,通過前面的分析,我們已經(jīng)知道他的作用是接收Generator并執(zhí)行,如果遇到take就將它注冊到channel上去,如果遇到put就將對應(yīng)的回調(diào)拿出來執(zhí)行,但是Redux-Saga又將這個(gè)過程分為了好幾層,我們一層一層來看吧。runSaga的參數(shù)先是通過bind傳入了一些上下文相關(guān)的變量,比如getState, dispatch,然后又在運(yùn)行的時(shí)候傳入了rootSaga,所以他應(yīng)該是長這個(gè)樣子的:
import proc from './proc';
export function runSaga(
{ channel, dispatch, getState },
saga,
...args
) {
// saga是一個(gè)Generator,運(yùn)行后得到一個(gè)迭代器
const iterator = saga(...args);
const env = {
channel,
dispatch,
getState,
};
proc(env, iterator);
}
可以看到runSaga僅僅是將Generator運(yùn)行下,得到迭代器對象后又調(diào)用了proc來處理。
runSaga對應(yīng)的源碼看這里:https://github.com/redux-saga/redux-saga/blob/master/packages/core/src/internal/runSaga.js
proc
proc就是具體執(zhí)行這個(gè)迭代器的過程,Generator的執(zhí)行方式我們之前在另一篇文章詳細(xì)講過,簡單來說就是可以另外寫一個(gè)方法next來執(zhí)行Generator,next里面檢測到如果Generator沒有執(zhí)行完,就繼續(xù)執(zhí)行next,然后外層調(diào)用一下next啟動(dòng)這個(gè)流程就行。
export default function proc(env, iterator) {
// 調(diào)用next啟動(dòng)迭代器執(zhí)行
next();
// next函數(shù)也不復(fù)雜
// 就是執(zhí)行iterator
function next(arg, isErr) {
let result;
if (isErr) {
result = iterator.throw(arg);
} else {
result = iterator.next(arg);
}
// 如果他沒結(jié)束,就繼續(xù)next
// digestEffect是處理當(dāng)前步驟返回值的函數(shù)
// 繼續(xù)執(zhí)行的next也由他來調(diào)用
if (!result.done) {
digestEffect(result.value, next)
}
}
}
digestEffect
上面如果迭代器沒有執(zhí)行完,我們會(huì)將它的值傳給digestEffect處理,那么這里的result.value的值是什么的呢?回想下我們前面rootSaga里面的用法
yield takeEvery("FETCH_USER_INFO", fetchUserInfo);
result.value的值應(yīng)該是yield后面的值,也就是takeEvery("FETCH_USER_INFO", fetchUserInfo)的返回值,takeEvery是再次包裝過的effect,他包裝了take,fork這些簡單的effect。其實(shí)對于像take這種簡單的effect來說,比如:
take("FETCH_USER_INFO", fetchUserInfo);
這行代碼的返回值直接就是一個(gè)對象,類似于這樣:
{
IO: true,
type: 'TAKE',
payload: {},
}
所以我們這里digestEffect拿到的result.value也是這樣的一個(gè)對象,這個(gè)對象就代表了我們的一個(gè)effect,所以我們的digestEffect就長這樣:
function digestEffect(effect, cb) { // 這個(gè)cb其實(shí)就是前面?zhèn)鬟M(jìn)來的next
// 這個(gè)變量是用來解決競爭問題的
let effectSettled;
function currCb(res, isErr) {
// 如果已經(jīng)運(yùn)行過了,直接return
if (effectSettled) {
return
}
effectSettled = true;
cb(res, isErr);
}
runEffect(effect, currCb);
}
runEffect
可以看到digestEffect又調(diào)用了一個(gè)函數(shù)runEffect,這個(gè)函數(shù)會(huì)處理具體的effect:
// runEffect就只是獲取對應(yīng)type的處理函數(shù),然后拿來處理當(dāng)前effect
function runEffect(effect, currCb) {
if (effect && effect.IO) {
const effectRunner = effectRunnerMap[effect.type]
effectRunner(env, effect.payload, currCb);
} else {
currCb();
}
}
這點(diǎn)代碼可以看出,runEffect也只是對effect進(jìn)行了檢測,通過他的類型獲取對應(yīng)的處理函數(shù),然后進(jìn)行處理,我這里代碼簡化了,只支持IO這種effect,官方源碼中還支持promise和iterator,具體的可以看看他的源碼:https://github.com/redux-saga/redux-saga/blob/master/packages/core/src/internal/proc.js
effectRunner
effectRunner是通過effect.type匹配出來的具體的effect的處理函數(shù),我們先來看兩個(gè):take和fork。
runTakeEffect
take的處理其實(shí)很簡單,就是將它注冊到我們的channel里面就行,所以我們建一個(gè)effectRunnerMap.js文件,在里面添加take的處理函數(shù)runTakeEffect:
// effectRunnerMap.js
function runTakeEffect(env, { channel = env.channel, pattern }, cb) {
const matcher = input => input.type === pattern;
// 注意channel.take的第二個(gè)參數(shù)是matcher
// 我們直接寫一個(gè)簡單的matcher,就是輸入類型必須跟pattern一樣才行
// 這里的pattern就是我們經(jīng)常用的action名字,比如FETCH_USER_INFO
// Redux-Saga不僅僅支持這種字符串,還支持多種形式,也可以自定義matcher來解析
channel.take(cb, matcher);
}
const effectRunnerMap = {
'TAKE': runTakeEffect,
};
export default effectRunnerMap;
注意上面代碼channel.take(cb, matcher);里面的cb,這個(gè)cb其實(shí)就是我們迭代器的next,也就是說take的回調(diào)是迭代器繼續(xù)執(zhí)行,也就是繼續(xù)執(zhí)行下面的代碼。也就是說,當(dāng)你這樣寫時(shí):
yield take("SOME_ACTION");
yield fork(saga);
當(dāng)運(yùn)行到yield take("SOME_ACTION");這行代碼時(shí),整個(gè)迭代器都阻塞了,不會(huì)再往下運(yùn)行。除非你觸發(fā)了SOME_ACTION,這時(shí)候會(huì)把SOME_ACTION的回調(diào)拿出來執(zhí)行,這個(gè)回調(diào)就是迭代器的next,所以就可以繼續(xù)執(zhí)行下面這行代碼了yield fork(saga)。
runForkEffect
我們前面的示例代碼其實(shí)沒有直接用到fork這個(gè)API,但是用到了takeEvery,takeEvery其實(shí)是組合take和fork來實(shí)現(xiàn)的,所以我們先來看看fork。fork的使用跟call很像,也是可以直接調(diào)用傳進(jìn)來的方法,只是call會(huì)等待結(jié)果回來才進(jìn)行下一步,fork不會(huì)阻塞這個(gè)過程,而是當(dāng)前結(jié)果沒回來也會(huì)直接運(yùn)行下一步:
fork(fn, ...args);
所以當(dāng)我們拿到fork的時(shí)候,處理起來也很簡單,直接調(diào)用proc處理fn就行了,fn應(yīng)該是一個(gè)Generator函數(shù)。
function runForkEffect(env, { fn }, cb) {
const taskIterator = fn(); // 運(yùn)行fn得到一個(gè)迭代器
proc(env, taskIterator); // 直接將taskIterator給proc處理
cb(); // 直接調(diào)用cb,不需要等待proc的結(jié)果
}
runPutEffect
我們前面的例子還用到了put這個(gè)effect,他就更簡單了,只是發(fā)出一個(gè)action,事實(shí)上他也是調(diào)用的Redux的dispatch來發(fā)出action:
function runPutEffect(env, { action }, cb) {
const result = env.dispatch(action); // 直接dispatch(action)
cb(result);
}
注意我們這里的代碼只需要dispatch(action)就行了,不需要再手動(dòng)調(diào)channel.put了,因?yàn)槲覀兦懊娴闹虚g件里面已經(jīng)改造了dispatch方法了,每次dispatch的時(shí)候都會(huì)自動(dòng)調(diào)用channel.put。
runCallEffect
前面我們發(fā)起API請求還用到了call,一般我們使用axios這種庫返回的都是一個(gè)promise,所以我們這里寫一種支持promise的情況,當(dāng)然普通同步函數(shù)肯定也是支持的:
function runCallEffect(env, { fn, args }, cb) {
const result = fn.apply(null, args);
if (isPromise(result)) {
return result
.then(data => cb(data))
.catch(error => cb(error, true));
}
cb(result);
}
這些effect具體處理的方法對應(yīng)的源碼都在這個(gè)文件里面:https://github.com/redux-saga/redux-saga/blob/master/packages/core/src/internal/effectRunnerMap.js
effects
上面我們講了幾個(gè)effect具體處理的方法,但是這些都不是對外暴露的effect API。真正對外暴露的effect API還需要單獨(dú)寫,他們其實(shí)都很簡單,都是返回一個(gè)帶有type的簡單對象就行:
const makeEffect = (type, payload) => ({
IO: true,
type,
payload
})
export function take(pattern) {
return makeEffect('TAKE', { pattern })
}
export function fork(fn) {
return makeEffect('FORK', { fn })
}
export function call(fn, ...args) {
return makeEffect('CALL', { fn, args })
}
export function put(action) {
return makeEffect('PUT', { action })
}
可以看到當(dāng)我們使用effect時(shí),他的返回值就僅僅是一個(gè)描述當(dāng)前任務(wù)的對象,這就讓我們的單元測試好寫很多。因?yàn)槲覀兊拇a在不同的環(huán)境下運(yùn)行可能會(huì)產(chǎn)生不同的結(jié)果,特別是這些異步請求,我們寫單元測試時(shí)來造這些數(shù)據(jù)也會(huì)很麻煩。但是如果你使用Redux-Saga的effect,每次你代碼運(yùn)行的時(shí)候得到的都是一個(gè)任務(wù)描述對象,這個(gè)對象是穩(wěn)定的,不受運(yùn)行結(jié)果影響,也就不需要針對這個(gè)造測試數(shù)據(jù)了,大大減少了工作量。
effects對應(yīng)的源碼文件看這里:https://github.com/redux-saga/redux-saga/blob/master/packages/core/src/internal/io.js
takeEvery
我們前面還用到了takeEvery來處理同時(shí)發(fā)起的多個(gè)請求,這個(gè)API是一個(gè)高級(jí)API,是封裝前面的take和fork來實(shí)現(xiàn)的,官方源碼又構(gòu)造了一個(gè)新的迭代器來組合他們,不是很直觀。官方文檔中的這種寫法反而很好理解,我這里采用文檔中的這種寫法:
export function takeEvery(pattern, saga) {
function* takeEveryHelper() {
while (true) {
yield take(pattern);
yield fork(saga);
}
}
return fork(takeEveryHelper);
}
上面這段代碼就很好理解了,我們一個(gè)死循環(huán)不停的監(jiān)聽pattern,即目標(biāo)事件,當(dāng)目標(biāo)事件過來的時(shí)候,就執(zhí)行對應(yīng)的saga,然后又進(jìn)入下一次循環(huán)繼續(xù)監(jiān)聽pattern。
總結(jié)
到這里我們例子中用到的API已經(jīng)全部自己實(shí)現(xiàn)了,我們可以用自己的這個(gè)Redux-Saga來替換官方的了,只是我們只實(shí)現(xiàn)了他的一部分功能,還有很多功能沒有實(shí)現(xiàn),不過這已經(jīng)不妨礙我們理解他的基本原理了。再來回顧下他的主要要點(diǎn):
Redux-Saga其實(shí)也是一個(gè)發(fā)布訂閱模式,管理事件的地方是channel,兩個(gè)重點(diǎn)API:take和put。take是注冊一個(gè)事件到channel上,當(dāng)事件過來時(shí)觸發(fā)回調(diào),需要注意的是,這里的回調(diào)僅僅是迭代器的next,并不是具體響應(yīng)事件的函數(shù)。也就是說take的意思就是:我在等某某事件,這個(gè)事件來之前不許往下走,來了后就可以往下走了。put是發(fā)出事件,他是使用Redux dispatch發(fā)出事件的,也就是說put的事件會(huì)被Redux和Redux-Saga同時(shí)響應(yīng)。Redux-Saga增強(qiáng)了Redux的dispatch函數(shù),在dispatch的同時(shí)會(huì)觸發(fā)channel.put,也就是讓Redux-Saga也響應(yīng)回調(diào)。- 我們調(diào)用的
effects和真正實(shí)現(xiàn)功能的函數(shù)是分開的,表層調(diào)用的effects只會(huì)返回一個(gè)簡單的對象,這個(gè)對象描述了當(dāng)前任務(wù),他是穩(wěn)定的,所以基于effects的單元測試很好寫。 - 當(dāng)拿到
effects返回的對象后,我們再根據(jù)他的type去找對應(yīng)的處理函數(shù)來進(jìn)行處理。 - 整個(gè)
Redux-Saga都是基于Generator的,每往下走一步都需要手動(dòng)調(diào)用next,這樣當(dāng)他執(zhí)行到中途的時(shí)候我們可以根據(jù)情況不再繼續(xù)調(diào)用next,這其實(shí)就相當(dāng)于將當(dāng)前任務(wù)cancel了。
本文可運(yùn)行的代碼已經(jīng)上傳到GitHub,可以拿下來玩玩:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/React/redux-saga
參考資料
Redux-Saga官方文檔:https://redux-saga.js.org/
Redux-Saga源碼地址: https://github.com/redux-saga/redux-saga/tree/master/packages/core/src
文章的最后,感謝你花費(fèi)寶貴的時(shí)間閱讀本文,如果本文給了你一點(diǎn)點(diǎn)幫助或者啟發(fā),請不要吝嗇你的贊和GitHub小星星,你的支持是作者持續(xù)創(chuàng)作的動(dòng)力。
歡迎關(guān)注我的公眾號(hào)進(jìn)擊的大前端第一時(shí)間獲取高質(zhì)量原創(chuàng)~
“前端進(jìn)階知識(shí)”系列文章:https://juejin.im/post/5e3ffc85518825494e2772fd
“前端進(jìn)階知識(shí)”系列文章源碼GitHub地址: https://github.com/dennis-jiang/Front-End-Knowledges

浙公網(wǎng)安備 33010602011771號(hào)