淺析redux-saga實現原理

blank

淺析redux-saga實現原理

作者簡介遠峰螞蟻金服數據前端

項目中一直使用redux-saga來處理異步action的流程。對於effect的實現原理感到很好奇。抽空去研究了一下他的實現。本文不會描述redux-saga的基礎API和優點,單純聊實現原理,歡迎大家在評論區留言討論。

前言

redux-saga監聽action的代碼如下:

import { takeEvery } from 'redux-saga'; function* mainSaga() { yield takeEvery('action_name', function* (action) { console.log(action); }); }

用generator究竟是怎麼實現takeEvery的呢?我們先來看稍微簡單一點的take的實現原理:

take實現原理

我們嘗試寫一個demo,用saga的方式實現用generator監聽action。

$btn.addEventListener('click', () => { const action =`action data${i++}`; // trigger action }, false); function* mainSaga() { const action = yield take(); console.log(action); }

要在$btn點擊時候,能夠讀到action的值。

channel

這裡我們需要引入一個概念——channel。

channel是對事件源的抽象,作用是先註冊一個take方法,當put觸發時,執行一次take方法,然後銷毀他。

channel的簡單實現如下:

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事件改寫如下:

$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。

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,這樣就完成了一次點擊事件的監聽過程。

查看在線demo

takeEvery實現原理

在上一節中,我們已經模仿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

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,從而做到源源不斷地監聽事件。

在線demo

effect的本質

通過上文的實現,我們發現所有的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的所有功能,只需要再添加一些細節就可以了。大概如下圖所示:

blank

對generator使用有興趣的同學推薦學習一下redux-saga源碼。在此推荐一篇使用generator實現dom事件監聽的文章繼續探索JS中的Iterator,兼談與Observable的對比

感興趣的同學可以關注專欄或者發送簡歷至chaofeng.lcf### alibaba-inc.com ,歡迎有誌之士加入~

原文地址: juejin.im/editor/posts/

What do you think?

Written by marketer

blank

使用Nuxt.js改善現有項目

blank

Node 調試指南—— Inspector 協議