做者簡介 joey 螞蟻金服·數據體驗技術團隊html
項目中一直使用redux-saga來處理異步action的流程。對於effect的實現原理感到很好奇。抽空去研究了一下他的實現。本文不會描述redux-saga的基礎API和優勢,單純聊實現原理,歡迎你們在評論區留言討論。git
redux-saga監聽action的代碼以下:github
import { takeEvery } from 'redux-saga';
function* mainSaga() {
yield takeEvery('action_name', function* (action) {
console.log(action);
});
}
複製代碼
用generator到底是怎麼實現takeEvery
的呢?咱們先來看稍微簡單一點的take
的實現原理:redux
咱們嘗試寫一個demo,用saga的方式實現用generator監聽action。bash
$btn.addEventListener('click', () => {
const action =`action data${i++}`;
// trigger action
}, false);
function* mainSaga() {
const action = yield take();
console.log(action);
}
複製代碼
要在$btn
點擊時候,可以讀到action的值。dom
這裏咱們須要引入一個概念——channel
。異步
channel是對事件源的抽象,做用是先註冊一個take方法,當put觸發時,執行一次take方法,而後銷燬他。學習
channel的簡單實現以下:ui
function channel() {
let taker;
function take(cb) {
taker = cb;
}
function put(input) {
if (taker) {
const tempTaker = taker;
taker = null;
tempTaker(input);
}
}
return {
put,
take,
};
}
const chan = channel();
複製代碼
咱們利用channel作generator和dom事件的鏈接,將dom事件改寫以下:spa
$btn.addEventListener('click', () => {
const action =`action data${i++}`;
chan.put(action);
}, false);
複製代碼
當put觸發時,若是channel裏已經有註冊了的taker,taker就會執行。
咱們須要在put觸發以前,先調用channel的take方法,註冊實際要運行的方法。
咱們繼續看mainSaga裏的實現。
function* mainSaga() {
const action = yield take();
console.log(action);
}
複製代碼
這個take是saga裏的一種effect類型。
先看effecttake()
的實現。
function take() {
return {
type: 'take'
};
}
複製代碼
出乎意料,僅僅返回了一個帶類型的object。
其實redux-saga裏全部effect返回的值,都是一個帶類型的純object對象。
那到底是何時觸發channel的take方法的呢?還須要從調用mainSaga的代碼上找緣由。
generator的特色是執行到某一步時,能夠把控制權交給外部代碼,由外部代碼拿到返回結果後,決定該怎麼作。
這裏咱們又要引入一個新的概念task
。
task
是generator方法的執行環境,全部saga的generator方法都跑在task裏。
task的簡易實現以下:
function task(iterator) {
const iter = iterator();
function next(args) {
const result = iter.next(args);
if (!result.done) {
const effect = result.value;
if (effect.type === 'take) { runTakeEffect(result.value, next); } } } next(); } task(mainSaga); 複製代碼
當yield take()
運行時,將take()
返回的結果交給外層的task,此時代碼的控制權就已經從gennerator方法中轉到了task裏了。
result.value
的值就是take()
返回的結果{ type: 'take' }
。
再看runTakeEffect
的實現:
function runTakeEffect(effect, cb) {
chan.take(input => {
cb(input);
});
}
複製代碼
到這裏,咱們終於看到調用channel的take方法的地方了。
完整代碼以下:
function channel() {
let taker;
function take(cb) {
taker = cb;
}
function put(input) {
if (taker) {
const tempTaker = taker;
taker = null;
tempTaker(input);
}
}
return {
put,
take,
};
}
const chan = channel();
function take() {
return {
type: 'take'
};
}
function* mainSaga() {
const action = yield take();
console.log(action);
}
function runTakeEffect(effect, cb) {
chan.take(input => {
cb(input);
});
}
function task(iterator) {
const iter = iterator();
function next(args) {
const result = iter.next(args);
if (!result.done) {
const effect = result.value;
if (effect.type === 'take') {
runTakeEffect(result.value, next);
}
}
}
next();
}
task(mainSaga);
let i = 0;
$btn.addEventListener('click', () => {
const action =`action data${i++}`;
chan.put(action);
}, false);
複製代碼
總體流程就是,先經過mainSaga往channel裏註冊了一個taker,一旦dom點擊發生,就觸發channel的put,put會消耗掉已經註冊的taker,這樣就完成了一次點擊事件的監聽過程。
在上一節中,咱們已經模仿saga實現了一次事件監聽,可是仍是有問題,咱們只能監聽一次點擊,怎麼能作到監聽每次點擊事件呢?redux-saga提供了一個helper方法——takeEvery
。咱們嘗試在咱們的簡易版saga中實現一下takeEvery
。
function* takeEvery(worker) {
yield fork(function* () {
while(true) {
const action = yield take();
worker(action);
}
});
}
function* mainSaga() {
yield takeEvery(action => {
$result.innerHTML = action;
});
}
複製代碼
這裏用到了一個新的effect方法fork
。
fork的做用是啓動一個新的task,不阻塞原task執行。代碼修改以下:
function fork(cb) {
return {
type: 'fork',
fn: cb,
};
}
function runForkEffect(effect, cb) {
task(effect.fn || effect);
cb();
}
function task(iterator) {
const iter = typeof iterator === 'function' ? iterator() : iterator;
function next(args) {
const result = iter.next(args);
if (!result.done) {
const effect = result.value;
// 判斷effect是不是iterator
if (typeof effect[Symbol.iterator] === 'function') {
runForkEffect(effect, next);
} else if (effect.type) {
switch (effect.type) {
case 'take':
runTakeEffect(effect, next);
break;
case 'fork':
runForkEffect(effect, next);
break;
default:
}
}
}
}
next();
}
複製代碼
咱們經過添加了一種新的effectfork
,啓動了一個新的task takeEvery。
takeEvery的做用就是當channel的put發生後,自動往channel裏放進一個新的taker。
咱們實現的channel裏同時只能有一個taker,while(true)
的做用就是每當一個put觸發消耗掉了taker後,就自動觸發runTakeEffect
中傳入的task的next方法,再次往channel裏放進一個taker,從而作到源源不斷地監聽事件。
經過上文的實現,咱們發現全部的yield後返回的effect,都是一個純object,用來給generator外層的執行容器task發送一個信號,告訴task該作什麼。
基於這種思路,若是咱們要新增一個effect,來cancel task,也能夠很容易實現。
首先咱們先定義一個cancel
方法,用來發送cancel的信號。
function cancel() {
return {
type: 'cancel'
};
}
複製代碼
而後修改task的代碼,讓他能真正執行cancel的邏輯。
function task(iterator) {
const iter = typeof iterator === 'function' ? iterator() : iterator;
...
function runCancelEffect() {
// do some cancel logic
}
function next(args) {
const result = iter.next(args);
if (!result.done) {
const effect = result.value;
if (typeof effect[Symbol.iterator] === 'function') {
runForkEffect(effect, next);
} else if (effect.type) {
switch (effect.type) {
case 'cancel':
runCancelEffect();
case 'take':
runTakeEffect(result.value, next);
break;
case 'fork':
runForkEffect(result.value, next);
break;
default:
}
}
}
}
next();
}
複製代碼
本文經過簡單實現了幾個effect方法來地介紹了redux-saga的原理,要真正作到redux-saga的全部功能,只須要再添加一些細節就能夠了。大概以下圖所示:
對generator使用有興趣的同窗推薦學習一下redux-saga
源碼。在此推薦一篇使用generator實現dom事件監聽的文章 繼續探索JS中的Iterator,兼談與Observable的對比
感興趣的同窗能夠關注專欄或者發送簡歷至'chaofeng.lcf####alibaba-inc.com'.replace('####', '@'),歡迎有志之士加入~