redux-saga 原理淺析
前言
筆者最近在做一些後臺專案,使用的是Ant Design Pro,其使用了redux-saga處理非同步資料流,本文將對redux-saga的原理做一個簡單的解讀,並將實現一個簡易版的redux-saga。
Generator函式的自動流程控制
在redux-saga中,saga是指一些長時操作,用generator函式表示。generator函式的強大之處在於其可以手動的暫停、恢復執行,且可以與函式體外進行資料互動,看如下例子:
function *gen() {
const a = yield 'hello';
console.log(a);
}
cont g = gen();
g.next(); // { value: 'hello', done: false }
setTimeout(() => g.next('hi'), 1000) // 此時 a => 'hi' 一秒後列印‘hi'
複製程式碼
可以看出來genrator函式何時進行下一步操作完全取決於外部的排程時機,且其內部執行狀態也由外部的輸入決定,這使得generator函式可以很方便的做非同步流程控制。舉個例子,我們首先讀取一個檔案的內容作為查詢引數,然後請求一個查詢介面並把返回的內容打印出來:
function getParams(file) {
return new Promise(resolve => {
fs.readFile(file, (err, data) => {
resolve(data)
})
})
}
function getContent(params) {
// request返回promise
return request(params)
}
function *gen() {
const params = yield getParams('config.json');
const content = yield getContent(params);
console.log(content);
}
複製程式碼
我們可以手動控制gen函式的執行:
const g = gen();
g.next().value.then(params => {
g.next(params).value.then(content => {
g.next(content);
})
})
複製程式碼
以上可以達到我們的目的,但是過於繁瑣,我們想要的是generator函式可以自動的執行,可以寫一個簡易的自動執行函式如下:
function genRun(gen) {
const g = gen();
next();
function next(err, pre) {
let temp;
(err === null) && (temp = g.next(pre));
(err !== null) && (temp = g.throw(pre));
if(!temp.done) {
nextWithYieldType(temp.value, next);
}
}
}
function nextWithYieldType(value, next) {
if(isPromise(value)) {
value
.then(success => next(null, success))
.catch(error => next(error))
}
}
genRun(gen);
複製程式碼
此時generator函式便可以自動執行,事實上我們可以發現,generator的內部狀態完全是由nextWithYieldType
決定的,我們可以根據yield的型別執行不同的處理邏輯。
Effect
事實上sagaMiddleware.run(saga)
可以類似看做genRun(saga)
,而saga是由一個個的effect組成的,那麼effect是什麼?redux-saga官網的解釋:一個 effect 就是一個 Plain Object JavaScript 物件,包含一些將被 saga middleware 執行的指令。redux-saga提供了很多Effect建立器,如call
、put
、take
等,已call
為例:
function saga*() {
const result = yield call(genPromise);
console.log(result);
}
複製程式碼
call(genPromise)
生成的就是一個effect,它可能類似如下:
{
isEffect: true,
type: 'CALL',
fn: genPromise
}
複製程式碼
事實上effect只表明了意圖,而實際的行為由類似於上文的nextWithYieldType完成,例如:
function nextWithYieldType(value, next) {
...
if(isCallEffect(value)) {
value.fn(). then(success => next(null, success)).catch(error => next(error))
}
}
複製程式碼
當genPromise函式返回的promise被resolve後便會打印出結果。
生產者與消費者
觀察下面的例子
function *saga() {
yield take('TEST');
console.log('test...');
}
sagaMiddleware.run(test);
複製程式碼
saga會在take('TEST')
處阻塞,只有執行了dispatch({type: 'TEST'})
後saga才能繼續執行(注意:此時的dispatch
方法是經過sagaMiddleware包裝過的)。這給我們的感覺似乎很像是take
是一個生產者,在等待disaptch
的消費,事實上take
只是一個Effect生成器,具體的處理邏輯依然是在nextWithYieldType完成的,類似於:
function nextWithYieldType(value, next) {
...
// take('TEST')生成的effect簡單的認為是 {isEffect: true, type: 'TAKE', name: 'TEST'}
if(isTakeEffect(value)) {
channel.take({pattern: value.name, cb: params => next(null, params)})
}
}
複製程式碼
channel是一個任務生成器,它有兩個方法:take生成任務,put消費任務:
function channel() {
/*
task = {
pattern,
cb
}
*/
let _task = null;
function take(task) {
_task = task;
}
function put(pattern, args) {
if(!_task) return;
if(pattern == _task.pattern) _task.cb.call(null, args);
}
return {
take,
put
}
}
複製程式碼
顯然任務是在執行dispatch
的時候被消費掉的,這個工作是在sagaMiddleware中做的,類似於如下:
const sagaMiddleware = store => {
return next => action => {
next(action);
const { type, ...payload } = action;
channel.put(type, payload);
}
}
複製程式碼
看到這裡我們可以發現,需要我們做的就是不斷的完善nextWithYieldType這個函式,當完成了put
、fork
、takeEvery
對應的邏輯後,一個具備基本功能的redux-saga就誕生啦,筆者就不在贅述這些功能的實現了。最後,你可以檢視這裡:tiny-redux-saga,這是筆者實現的一個簡易版的redux-saga,希望對你有所幫助。
全文完。