1. 程式人生 > >手寫Redux-Saga原始碼

手寫Redux-Saga原始碼

[上一篇文章我們分析了`Redux-Thunk`的原始碼](https://juejin.im/post/6869950884231675912),可以看到他的程式碼非常簡單,只是讓`dispatch`可以處理函式型別的`action`,其作者也承認對於複雜場景,`Redux-Thunk`並不適用,還推薦了`Redux-Saga`來處理複雜副作用。本文要講的就是`Redux-Saga`,這個也是我在實際工作中使用最多的`Redux`非同步解決方案。`Redux-Saga`比`Redux-Thunk`複雜得多,而且他整個非同步流程都使用`Generator`來處理,`Generator`也是我們這篇文章的前置知識,[如果你對`Generator`還不熟悉,可以看看這篇文章](https://juejin.im/post/6844904133577670664)。 本文仍然是老套路,先來一個`Redux-Saga`的簡單例子,然後我們自己寫一個`Redux-Saga`來替代他,也就是原始碼分析。 本文可執行的程式碼已經上傳到GitHub,可以拿下來玩玩:[https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/React/redux-saga](https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/React/redux-saga) ## 簡單例子 網路請求是我們經常需要處理的非同步操作,假設我們現在的一個簡單需求就是點選一個按鈕去請求使用者的資訊,大概長這樣: ![Sep-11-2020 16-31-55](//p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6b24cab49c86442188f6b1f74b377306~tplv-k3u1fbpfcp-zoom-1.image) 這個需求使用`Redux`實現起來也很簡單,點選按鈕的時候`dispatch`出一個`action`。這個`action`會觸發一個請求,請求返回的資料拿來顯示在頁面上就行: ```javascript import React from 'react'; import { connect } from 'react-redux'; function App(props) { const { dispatch, userInfo } = props; const getUserInfo = () => { dispatch({ type: 'FETCH_USER_INFO' }) } return (

{userInfo && JSON.stringify(userInfo)} ); } const matStateToProps = (state) => ({ userInfo: state.userInfo }) export default connect(matStateToProps)(App); ``` [上面這種寫法都是我們之前講`Redux`就介紹過的](https://juejin.im/post/6847902222756347911),`Redux-Saga`介入的地方是`dispatch({ type: 'FETCH_USER_INFO' })`之後。按照`Redux`一般的流程,`FETCH_USER_INFO`被髮出後應該進入`reducer`處理,但是`reducer`都是同步程式碼,並不適合發起網路請求,所以我們可以使用`Redux-Saga`來捕獲`FETCH_USER_INFO`並處理。 `Redux-Saga`是一個`Redux`中介軟體,所以我們在`createStore`的時候將它引入就行: ```javascript // 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後 // 還需要手動啟動他來執行rootSaga sagaMiddleware.run(rootSaga); export default store; ``` 注意上面程式碼裡的這一行: ```javascript sagaMiddleware.run(rootSaga); ``` `sagaMiddleware.run`是用來手動啟動`rootSaga`的,我們來看看`rootSaga`是怎麼寫的: ```javascript 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`這個`Generator`函式,這裡面就一行: ```javascript yield takeEvery("FETCH_USER_INFO", fetchUserInfo); ``` 這一行程式碼用到了`Redux-Saga`的一個`effect`,也就是`takeEvery`,他的作用是監聽**每個**`FETCH_USER_INFO`,當`FETCH_USER_INFO`出現的時候,就呼叫`fetchUserInfo`函式,注意這裡是**每個**`FETCH_USER_INFO`。也就是說如果同時發出多個`FETCH_USER_INFO`,我們每個都會響應併發起請求。類似的還有`takeLatest`,`takeLatest`從名字都可以看出來,是響應最後一個請求,具體使用哪一個,要看具體的需求。 然後看看`fetchUserInfo`函式,這個函式也不復雜,就是呼叫一個`API`函式`fetchUserInfoAPI`去獲取資料,注意我們這裡函式呼叫並不是直接的`fetchUserInfoAPI()`,而是使用了`Redux-Saga`的`call`這個`effect`,這樣做可以讓我們寫單元測試變得更簡單,為什麼會這樣,我們後面講原始碼的時候再來仔細看看。獲取資料後,我們呼叫了`put`去發出`FETCH_USER_SUCCEEDED`這個`action`,這裡的`put`類似於`Redux`裡面的`dispatch`,也是用來發出`action`的。這樣我們的`reducer`就可以拿到`FETCH_USER_SUCCEEDED`進行處理了,跟以前的`reducer`並沒有太大區別。 ```javascript // 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; ``` 通過這個例子的程式碼結構我們可以看出: > 1. `action`被分為了兩種,一種是觸發非同步處理的,一種是普通的同步`action`。 > > 2. 非同步`action`使用`Redux-Saga`來監聽,監聽的時候可以使用`takeLatest`或者`takeEvery`來處理併發的請求。 > > 3. 具體的`saga`實現可以使用`Redux-Saga`提供的方法,比如`call`,`put`之類的,可以讓單元測試更好寫。 > > 4. 一個`action`可以被`Redux-Saga`和`Reducer`同時響應,比如上面的`FETCH_USER_INFO`發出後我還想讓頁面轉個圈,可以直接在`reducer`裡面加一個就行: > > ```javascript > ... > case 'FETCH_USER_INFO': > return { ...state, isLoading: true }; > ... > ``` ## 手寫原始碼 通過上面這個例子,我們可以看出,`Redux-Saga`的執行是通過這一行程式碼來實現的: ```javascript sagaMiddleware.run(rootSaga); ``` 整個`Redux-Saga`的執行和原本的`Redux`並不衝突,`Redux`甚至都不知道他的存在,他們之間耦合很小,只在需要的時候通過`put`發出`action`來進行通訊。所以我猜測,他應該是自己實現了一套完全獨立的非同步任務處理機制,下面我們從能感知到的`API`入手,一步一步來探尋下他原始碼的奧祕吧。本文全部程式碼參照官方原始碼寫成,函式名字和變數名字儘量保持一致,寫到具體的方法的時候我也會貼出對應的程式碼地址,主要程式碼都在這裡:[https://github.com/redux-saga/redux-saga/tree/master/packages/core/src](https://github.com/redux-saga/redux-saga/tree/master/packages/core/src) 先來看看我們用到了哪些`API`,這些API就是我們今天手寫的目標: > 1. **createSagaMiddleware**:這個方法會返回一箇中間件例項`sagaMiddleware` > 2. **sagaMiddleware.run**: 這個方法是真正執行我們寫的`saga`的入口 > 3. **takeEvery**:這個方法是用來控制併發流程的 > 4. **call**:用來呼叫其他方法 > 5. **put**:發出`action`,用來和`Redux`通訊 ### 從中介軟體入手 [之前我們講`Redux`原始碼的時候詳細分析了`Redux`中介軟體的原理和正規化](https://juejin.im/post/6845166891682512909#heading-7),一箇中間件大概就長這個樣子: ```javascript 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 } } } ``` 這其實就相當於一個`Redux`中介軟體的正規化了: > 1. 一箇中間件接收`store`作為引數,會返回一個函式 > 2. 返回的這個函式接收老的`dispatch`函式作為引數(也就是上面的`next`),會返回一個新的函式 > 3. 返回的新函式就是新的`dispatch`函式,這個函式裡面可以拿到外面兩層傳進來的`store`和老`dispatch`函式 依照這個正規化以及前面對`createSagaMiddleware`的使用,我們可以先寫出這個函式的骨架: ```javascript // sagaMiddlewareFactory其實就是我們外面使用的createSagaMiddleware function sagaMiddlewareFactory() { // 返回的是一個Redux中介軟體 // 需要符合他的正規化 const sagaMiddleware = function (store) { return function (next) { return function (action) { // 內容先寫個空的 let result = next(action); return result; } } } // sagaMiddleware上還有個run方法 // 是用來啟動saga的 // 我們先留空吧 sagaMiddleware.run = () => { } return sagaMiddleware; } export default sagaMiddlewareFactory; ``` ### 梳理架構 現在我們有了一個空的骨架,接下來該幹啥呢?前面我們說過了,`Redux-Saga`很可能是自己實現了一套完全獨立的非同步事件處理機制。這種非同步事件處理機制需要一個處理中心來儲存事件和處理函式,還需要一個方法來觸發佇列中的事件的執行,再回看前面的使用的API,我們發現了兩個類似功能的API: > 1. **takeEvery(action, callback)**:他接收的引數就是`action`和`callback`,而且我們在根`saga`裡面可能會多次呼叫它來註冊不同`action`的處理函式,這其實就相當於往處理中心裡面塞入事件了。 > 2. **put(action)**:`put`的引數是`action`,他唯一的作用就是觸發對應事件的回撥執行。 可以看到`Redux-Saga`這種機制也是用`takeEvery`先註冊回撥,然後使用`put`發出訊息來觸發回撥執行,這其實跟我們其他文章多次提到的釋出訂閱模式很像。 ### 手寫channel `channel`是`Redux-Saga`儲存回撥和觸發回撥的地方,類似於釋出訂閱模式,我們先來寫個: ```javascript export function multicastChannel() { const currentTakers = []; // 一個變數儲存我們所有註冊的事件和回撥 // 儲存事件和回撥的函式 // Redux-Saga裡面take接收回調cb和匹配方法matcher兩個引數 // 事實上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塞進來的匹配方法 // 如果匹配上了就將回調拿出來執行 if (taker['MATCH'](input)) { taker(input); } } } return { take, put } } ``` 上述程式碼中有一個奇怪的點,就是將`matcher`作為屬性放到了回撥函式上,這麼做的原因我想是為了讓外部可以自定義匹配方法,而不是簡單的事件名稱匹配,事實上`Redux-Saga`本身就支援好幾種匹配模式,包括`字串,Symbol,陣列`等等。 內建支援的匹配方法可以看這裡:[https://github.com/redux-saga/redux-saga/blob/master/packages/core/src/internal/matcher.js](https://github.com/redux-saga/redux-saga/blob/master/packages/core/src/internal/matcher.js)。 `channel`對應的原始碼可以看這裡:[https://github.com/redux-saga/redux-saga/blob/master/packages/core/src/internal/channel.js#L153](https://github.com/redux-saga/redux-saga/blob/master/packages/core/src/internal/channel.js#L153) 有了`channel`之後,我們的中介軟體裡面其實只要再幹一件事情就行了,就是呼叫`channel.put`將接收的`action`再發給`channel`去執行回撥就行,所以我們加一行程式碼: ```javascript // ... 省略前面程式碼 const result = next(action); channel.put(action); // 將收到的action也發給Redux-Saga return result; // ... 省略後面程式碼 ``` ### sagaMiddleware.run 前面的`put`是發出事件,執行回撥,可是我們的回撥還沒註冊呢,那註冊回撥應該在什麼地方呢?看起來只有一個地方了,那就是`sagaMiddleware.run`。簡單來說,`sagaMiddleware.run`接收一個`Generator`作為引數,然後執行這個`Generator`,當遇到`take`的時候就將它註冊到`channel`上面去。這裡我們先實現`take`,`takeEvery`是在這個基礎上實現的。`Redux-Saga`中這塊程式碼是單獨抽取了一個檔案,我們仿照這種做法吧。 首先需要在中介軟體裡面將`Redux`的`getState`和`dispatch`等引數傳遞進去,`Redux-Saga`使用的是`bind`函式,所以中介軟體方法改造如下: ```javascript 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`拿來執行就行了: ```javascript sagaMiddleware.run = (...args) => { boundRunSaga(...args) } ``` 注意這裡的`...args`,這個其實就是我們傳進去的`rootSaga`。到這裡其實中介軟體部分就已經完成了,後面的程式碼就是具體的執行過程了。 中介軟體對應的原始碼可以看這裡:[https://github.com/redux-saga/redux-saga/blob/master/packages/core/src/internal/middleware.js](https://github.com/redux-saga/redux-saga/blob/master/packages/core/src/internal/middleware.js) ### runSaga `runSaga`其實才是真正的`sagaMiddleware.run`,通過前面的分析,我們已經知道他的作用是接收`Generator`並執行,如果遇到`take`就將它註冊到`channel`上去,如果遇到`put`就將對應的回撥拿出來執行,但是`Redux-Saga`又將這個過程分為了好幾層,我們一層一層來看吧。`runSaga`的引數先是通過`bind`傳入了一些上下文相關的變數,比如`getState, dispatch`,然後又在執行的時候傳入了`rootSaga`,所以他應該是長這個樣子的: ```javascript import proc from './proc'; export function runSaga( { channel, dispatch, getState }, saga, ...args ) { // saga是一個Generator,執行後得到一個迭代器 const iterator = saga(...args); const env = { channel, dispatch, getState, }; proc(env, iterator); } ``` 可以看到`runSaga`僅僅是將`Generator`執行下,得到迭代器物件後又呼叫了`proc`來處理。 `runSaga`對應的原始碼看這裡:[https://github.com/redux-saga/redux-saga/blob/master/packages/core/src/internal/runSaga.js](https://github.com/redux-saga/redux-saga/blob/master/packages/core/src/internal/runSaga.js) ### proc `proc`就是具體執行這個迭代器的過程,`Generator`的執行方式我們之前[在另一篇文章詳細講過](https://juejin.im/post/6844904133577670664),簡單來說就是可以另外寫一個方法`next`來執行`Generator`,`next`裡面檢測到如果`Generator`沒有執行完,就繼續執行`next`,然後外層呼叫一下`next`啟動這個流程就行。 ```javascript export default function proc(env, iterator) { // 呼叫next啟動迭代器執行 next(); // next函式也不復雜 // 就是執行iterator function next(arg, isErr) { let result; if (isErr) { result = iterator.throw(arg); } else { result = iterator.next(arg); } // 如果他沒結束,就繼續next // digestEffect是處理當前步驟返回值的函式 // 繼續執行的next也由他來呼叫 if (!result.done) { digestEffect(result.value, next) } } } ``` #### digestEffect 上面如果迭代器沒有執行完,我們會將它的值傳給`digestEffect`處理,那麼這裡的`result.value`的值是什麼的呢?回想下我們前面`rootSaga`裡面的用法 ```javascript yield takeEvery("FETCH_USER_INFO", fetchUserInfo); ``` `result.value`的值應該是`yield`後面的值,也就是`takeEvery("FETCH_USER_INFO", fetchUserInfo)`的返回值,`takeEvery`是再次包裝過的`effect`,他包裝了`take,fork`這些簡單的`effect`。其實對於像`take`這種簡單的`effect`來說,比如: ```javascript take("FETCH_USER_INFO", fetchUserInfo); ``` 這行程式碼的返回值直接就是一個物件,類似於這樣: ```javascript { IO: true, type: 'TAKE', payload: {}, } ``` 所以我們這裡`digestEffect`拿到的`result.value`也是這樣的一個物件,這個物件就代表了我們的一個`effect`,所以我們的`digestEffect`就長這樣: ```javascript function digestEffect(effect, cb) { // 這個cb其實就是前面傳進來的next // 這個變數是用來解決競爭問題的 let effectSettled; function currCb(res, isErr) { // 如果已經執行過了,直接return if (effectSettled) { return } effectSettled = true; cb(res, isErr); } runEffect(effect, currCb); } ``` #### runEffect 可以看到`digestEffect`又呼叫了一個函式`runEffect`,這個函式會處理具體的`effect`: ```javascript // runEffect就只是獲取對應type的處理函式,然後拿來處理當前effect function runEffect(effect, currCb) { if (effect && effect.IO) { const effectRunner = effectRunnerMap[effect.type] effectRunner(env, effect.payload, currCb); } else { currCb(); } } ``` 這點程式碼可以看出,`runEffect`也只是對`effect`進行了檢測,通過他的型別獲取對應的處理函式,然後進行處理,我這裡程式碼簡化了,只支援`IO`這種`effect`,官方原始碼中還支援`promise`和`iterator`,具體的可以看看他的原始碼:[https://github.com/redux-saga/redux-saga/blob/master/packages/core/src/internal/proc.js](https://github.com/redux-saga/redux-saga/blob/master/packages/core/src/internal/proc.js) ### effectRunner `effectRunner`是通過`effect.type`匹配出來的具體的`effect`的處理函式,我們先來看兩個:`take`和`fork`。 #### runTakeEffect `take`的處理其實很簡單,就是將它註冊到我們的`channel`裡面就行,所以我們建一個`effectRunnerMap.js`檔案,在裡面新增`take`的處理函式`runTakeEffect`: ```javascript // effectRunnerMap.js function runTakeEffect(env, { channel = env.channel, pattern }, cb) { const matcher = input => input.type === pattern; // 注意channel.take的第二個引數是matcher // 我們直接寫一個簡單的matcher,就是輸入型別必須跟pattern一樣才行 // 這裡的pattern就是我們經常用的action名字,比如FETCH_USER_INFO // Redux-Saga不僅僅支援這種字串,還支援多種形式,也可以自定義matcher來解析 channel.take(cb, matcher); } const effectRunnerMap = { 'TAKE': runTakeEffect, }; export default effectRunnerMap; ``` 注意上面程式碼`channel.take(cb, matcher);`裡面的`cb`,這個`cb`其實就是我們迭代器的`next`,也就是說`take`的回撥是迭代器繼續執行,也就是繼續執行下面的程式碼。也就是說,當你這樣寫時: ```javascript yield take("SOME_ACTION"); yield fork(saga); ``` 當執行到`yield take("SOME_ACTION");`這行程式碼時,整個迭代器都阻塞了,不會再往下執行。除非你觸發了`SOME_ACTION`,這時候會把`SOME_ACTION`的回撥拿出來執行,這個回撥就是迭代器的`next`,所以就可以繼續執行下面這行程式碼了`yield fork(saga)`。 #### runForkEffect 我們前面的示例程式碼其實沒有直接用到`fork`這個API,但是用到了`takeEvery`,`takeEvery`其實是組合`take`和`fork`來實現的,所以我們先來看看`fork`。`fork`的使用跟`call`很像,也是可以直接呼叫傳進來的方法,只是`call`會等待結果回來才進行下一步,`fork`不會阻塞這個過程,而是當前結果沒回來也會直接執行下一步: ```javascript fork(fn, ...args); ``` 所以當我們拿到`fork`的時候,處理起來也很簡單,直接呼叫`proc`處理`fn`就行了,`fn`應該是一個`Generator`函式。 ```javascript function runForkEffect(env, { fn }, cb) { const taskIterator = fn(); // 執行fn得到一個迭代器 proc(env, taskIterator); // 直接將taskIterator給proc處理 cb(); // 直接呼叫cb,不需要等待proc的結果 } ``` #### runPutEffect 我們前面的例子還用到了`put`這個`effect`,他就更簡單了,只是發出一個`action`,事實上他也是呼叫的`Redux`的`dispatch`來發出`action`: ```javascript function runPutEffect(env, { action }, cb) { const result = env.dispatch(action); // 直接dispatch(action) cb(result); } ``` 注意我們這裡的程式碼只需要`dispatch(action)`就行了,不需要再手動調`channel.put`了,因為我們前面的中介軟體裡面已經改造了`dispatch`方法了,每次`dispatch`的時候都會自動呼叫`channel.put`。 #### runCallEffect 前面我們發起`API`請求還用到了`call`,一般我們使用`axios`這種庫返回的都是一個`promise`,所以我們這裡寫一種支援`promise`的情況,當然普通同步函式肯定也是支援的: ```javascript 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`具體處理的方法對應的原始碼都在這個檔案裡面:[https://github.com/redux-saga/redux-saga/blob/master/packages/core/src/internal/effectRunnerMap.js](https://github.com/redux-saga/redux-saga/blob/master/packages/core/src/internal/effectRunnerMap.js) ### effects 上面我們講了幾個`effect`具體處理的方法,但是這些都不是對外暴露的`effect API`。真正對外暴露的`effect API`還需要單獨寫,他們其實都很簡單,都是返回一個帶有`type`的簡單物件就行: ```javascript 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 }) } ``` 可以看到當我們使用`effect`時,他的返回值就僅僅是一個描述當前任務的物件,這就讓我們的單元測試好寫很多。因為我們的程式碼在不同的環境下執行可能會產生不同的結果,特別是這些非同步請求,我們寫單元測試時來造這些資料也會很麻煩。但是如果你使用`Redux-Saga`的`effect`,每次你程式碼執行的時候得到的都是一個任務描述物件,這個物件是穩定的,不受執行結果影響,也就不需要針對這個造測試資料了,大大減少了工作量。 `effects`對應的原始碼檔案看這裡:[https://github.com/redux-saga/redux-saga/blob/master/packages/core/src/internal/io.js](https://github.com/redux-saga/redux-saga/blob/master/packages/core/src/internal/io.js) ### takeEvery 我們前面還用到了`takeEvery`來處理同時發起的多個請求,這個`API`是一個高階API,是封裝前面的`take`和`fork`來實現的,[官方原始碼又構造了一個新的迭代器來組合他們](https://github.com/redux-saga/redux-saga/blob/master/packages/core/src/internal/sagaHelpers/takeEvery.js),不是很直觀。[官方文件中的這種寫法反而很好理解](https://redux-saga.js.org/docs/advanced/Concurrency.html),我這裡採用文件中的這種寫法: ```javascript export function takeEvery(pattern, saga) { function* takeEveryHelper() { while (true) { yield take(pattern); yield fork(saga); } } return fork(takeEveryHelper); } ``` 上面這段程式碼就很好理解了,我們一個死迴圈不停的監聽`pattern`,即目標事件,當目標事件過來的時候,就執行對應的`saga`,然後又進入下一次迴圈繼續監聽`pattern`。 ## 總結 到這裡我們例子中用到的`API`已經全部自己實現了,我們可以用自己的這個`Redux-Saga`來替換官方的了,只是我們只實現了他的一部分功能,還有很多功能沒有實現,不過這已經不妨礙我們理解他的基本原理了。再來回顧下他的主要要點: 1. `Redux-Saga`其實也是一個釋出訂閱模式,管理事件的地方是`channel`,兩個重點`API`:`take`和`put`。 2. `take`是註冊一個事件到`channel`上,當事件過來時觸發回撥,需要注意的是,這裡的回撥僅僅是迭代器的`next`,並不是具體響應事件的函式。也就是說`take`的意思就是:我在等某某事件,這個事件來之前不許往下走,來了後就可以往下走了。 3. `put`是發出事件,他是使用`Redux dispatch`發出事件的,也就是說`put`的事件會被`Redux`和`Redux-Saga`同時響應。 4. `Redux-Saga`增強了`Redux`的`dispatch`函式,在`dispatch`的同時會觸發`channel.put`,也就是讓`Redux-Saga`也響應回撥。 5. 我們呼叫的`effects`和真正實現功能的函式是分開的,表層呼叫的`effects`只會返回一個簡單的物件,這個物件描述了當前任務,他是穩定的,所以基於`effects`的單元測試很好寫。 6. 當拿到`effects`返回的物件後,我們再根據他的`type`去找對應的處理函式來進行處理。 7. 整個`Redux-Saga`都是基於`Generator`的,每往下走一步都需要手動呼叫`next`,這樣當他執行到中途的時候我們可以根據情況不再繼續呼叫`next`,這其實就相當於將當前任務`cancel`了。 本文可執行的程式碼已經上傳到GitHub,可以拿下來玩玩:[https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/React/redux-saga](https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/React/redux-saga) ## 參考資料 `Redux-Saga`官方文件:[https://redux-saga.js.org/](https://redux-saga.js.org/) `Redux-Saga`原始碼地址: [https://github.com/redux-saga/redux-saga/tree/master/packages/core/src](https://github.com/redux-saga/redux-saga/tree/master/packages/core/src) **文章的最後,感謝你花費寶貴的時間閱讀本文,如果本文給了你一點點幫助或者啟發,請不要吝嗇你的贊和GitHub小星星,你的支援是作者持續創作的動力。** **作者博文GitHub專案地址: [https://github.com/dennis-jiang/Front-End-Knowledges](https://github.com/dennis-jiang/Front-End-Knowledges)** **作者掘金文章彙總:[https://juejin.im/post/5e3ffc85518825494e2772fd](https://juejin.im/post/5e3ffc85518825494e2772fd)** **我也搞了個公眾號[進擊的大前端],不打廣告,不寫水文,只發高質量原創,歡迎