
讓我驚訝的是,redux-saga 的作者竟然是一名金融出身的在一家房地產(chǎn)公司工作的員工(讓我想到了阮老師。。。),但是他對寫代碼有著非常濃厚的熱忱,喜歡學(xué)習(xí)和挑戰(zhàn)新的事物,并探索新的想法。恩,牛逼的人不需要解釋。
1. 介紹
對于從來沒有聽說過 redux-saga 的人,作者會如何描述它呢?
It is a Redux middleware for handling side effects. —— Yassine Elouafi
這里包含了兩個信息:
首先,redux-saga 是一個 redux 的中間件,而中間件的作用是為 redux 提供額外的功能。
其次,我們都知道,在 reducers 中的所有操作都是同步的并且是純粹的,即 reducer 都是純函數(shù),純函數(shù)是指一個函數(shù)的返回結(jié)果只依賴于它的參數(shù),并且在執(zhí)行過程中不會對外部產(chǎn)生副作用,即給它傳什么,就吐出什么。但是在實(shí)際的應(yīng)用開發(fā)中,我們希望做一些異步的(如Ajax請求)且不純粹的操作(如改變外部的狀態(tài)),這些在函數(shù)式編程范式中被稱為“副作用”。
Redux 的作者將這些副作用的處理通過提供中間件的方式讓開發(fā)者自行選擇進(jìn)行實(shí)現(xiàn)。
redux-saga 就是用來處理上述副作用(異步任務(wù))的一個中間件。它是一個接收事件,并可能觸發(fā)新事件的過程管理者,為你的應(yīng)用管理復(fù)雜的流程。
2. 先說一說 redux-thunk
redux-thunk 和 redux-saga 是 redux 應(yīng)用中最常用的兩種異步流處理方式。
From a synchronous perspective, a Thunk is a function that has everything already that it needs to do to give you some value back. You do not need to pass any arguments in, you simply call it and it will give you value back.
從異步的角度,Thunk 是指一切都就緒的會返回某些值的函數(shù)。你不用傳任何參數(shù),你只需調(diào)用它,它便會返回相應(yīng)的值?!?Rethinking Asynchronous Javascript
redux-thunk 的任務(wù)執(zhí)行方式是從 UI 組件直接觸發(fā)任務(wù)。
舉個栗子:
假如當(dāng)每次 Button 被點(diǎn)擊的時候,我們想要從給定的 url 中獲取數(shù)據(jù),采用 redux-thunk, 我們會這樣寫:
// fetchUrl 返回一個 thunk
function fetchUrl(url) {
return (dispatch) => {
dispatch({
type: 'FETCH_REQUEST'
});
fetch(url).then(data => dispatch({
type: 'FETCH_SUCCESS',
data
}));
}
}
// 如果 thunk 中間件正在運(yùn)行的話,我們可以 dispatch 上述函數(shù)如下:
dispatch(
fetchUrl(url)
):
redux-thunk 的主要思想是擴(kuò)展 action,使得 action 從一個對象變成一個函數(shù)。
另一個較完整的栗子:
// redux-thunk example
import {applyMiddleware, createStore} from 'redux';
import axios from 'axios';
import thunk from 'redux-thunk';
const initialState = { fetching: false, fetched: false, users: [], error: null }
const reducer = (state = initialState, action) => {
switch(action.type) {
case 'FETCH_USERS_START': {
return {...state, fetching: true}
break;
}
case 'FETCH_USERS_ERROR': {
return {...state, fetching: false, error: action.payload}
break;
}
case 'RECEIVE_USERS': {
return {...state, fetching: false, fetched: true, users: action.payload}
break;
}
}
return state;
}
const middleware = applyMiddleware(thunk);
// store.dispatch({type: 'FOO'});
// redux-thunk 的作用即是將 action 從一個對象變成一個函數(shù)
store.dispatch((dispatch) => {
dispatch({type: 'FETCH_USERS_START'});
// do something async
axios.get('http://rest.learncode.academy/api/wstern/users')
.then((response) => {
dispatch({type: 'RECEIVE_USERS', payload: response.data})
})
.catch((err) => {
dispatch({type: 'FECTH_USERS_ERROR', payload: err})
})
});
redux-thunk 的缺點(diǎn):
(1)action 雖然擴(kuò)展了,但因此變得復(fù)雜,后期可維護(hù)性降低;
(2)thunks 內(nèi)部測試邏輯比較困難,需要mock所有的觸發(fā)函數(shù);
(3)協(xié)調(diào)并發(fā)任務(wù)比較困難,當(dāng)自己的 action 調(diào)用了別人的 action,別人的 action 發(fā)生改動,則需要自己主動修改;
(4)業(yè)務(wù)邏輯會散布在不同的地方:啟動的模塊,組件以及thunks內(nèi)部。
3. redux-saga 是如何工作的?
sages 采用 Generator 函數(shù)來 yield Effects(包含指令的文本對象)。Generator 函數(shù)的作用是可以暫停執(zhí)行,再次執(zhí)行的時候從上次暫停的地方繼續(xù)執(zhí)行。Effect 是一個簡單的對象,該對象包含了一些給 middleware 解釋執(zhí)行的信息。你可以通過使用 effects API 如 fork,call,take,put,cancel 等來創(chuàng)建 Effect。( redux-saga API 參考)
如 yield call(fetch, '/products') 即 yield 了下面的對象,call 創(chuàng)建了一條描述結(jié)果的信息,然后,redux-saga middleware 將確保執(zhí)行這些指令并將指令的結(jié)果返回給 Generator:
// Effect -> 調(diào)用 fetch 函數(shù)并傳遞 `./products` 作為參數(shù)
{
type: CALL,
function: fetch,
args: ['./products']
}
與 redux-thunk 不同的是,在 redux-saga 中,UI 組件自身從來不會觸發(fā)任務(wù),它們總是會 dispatch 一個 action 來通知在 UI 中哪些地方發(fā)生了改變,而不需要對 action 進(jìn)行修改。redux-saga 將異步任務(wù)進(jìn)行了集中處理,且方便測試。
dispacth({ type: 'FETCH_REQUEST', url: /* ... */} );
所有的東西都必須被封裝在 sagas 中。sagas 包含3個部分,用于聯(lián)合執(zhí)行任務(wù):
- worker saga
做所有的工作,如調(diào)用 API,進(jìn)行異步請求,并且獲得返回結(jié)果- watcher saga
監(jiān)聽被 dispatch 的 actions,當(dāng)接收到 action 或者知道其被觸發(fā)時,調(diào)用 worker saga 執(zhí)行任務(wù)- root saga
立即啟動 sagas 的唯一入口
? 如何使用?
首先,我們得在文件入口中加入 saga 中間件,并且啟動它,它會一直運(yùn)行:
//...
import { createStore, applyMiddleware} from 'redux';
import createSagaMiddleware from 'redux-saga';
import appReducer from './reducers';
//...
const sagaMiddleware = createSagaMiddleware();
const middlewares = [sagaMiddleware];
const store = createStore(appReducer, applyMiddleware(...middlewares));
sagaMiddleware.run(rootSaga);
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('app')
);
然后,就可以在 sagas 文件夾中集中寫 saga 文件了:
// example 1
import { take, fork, call, put } from 'redux-saga/effects';
// The worker: perform the requested task
function* fetchUrl(url) {
const data = yield call(fetch, url); // 指示中間件調(diào)用 fetch 異步任務(wù)
yield put({ type: 'FETCH_SUCCESS', data }); // 指示中間件發(fā)起一個 action 到 Store
}
// The watcher: watch actions and coordinate worker tasks
function* watchFetchRequests() {
while(true) {
const action = yield take('FETCH_REQUEST'); // 指示中間件等待 Store 上指定的 action,即監(jiān)聽 action
yield fork(fetchUrl, action.url); // 指示中間件以無阻塞調(diào)用方式執(zhí)行 fetchUrl
}
}
在 redux-saga 中的基本概念就是:sagas 自身不真正執(zhí)行副作用(如函數(shù) call),但是會構(gòu)造一個需要執(zhí)行作用的描述。中間件會執(zhí)行該副作用并把結(jié)果返回給 generator 函數(shù)。
對上述例子的說明:
(1)引入的 redux-saga/effects 都是純函數(shù),每個函數(shù)構(gòu)造一個特殊的對象,其中包含著中間件需要執(zhí)行的指令,如:call(fetchUrl, url) 返回一個類似于 {type: CALL, function: fetchUrl, args: [url]} 的對象。
(2)在 watcher saga watchFetchRequests中:
首先 yield take('FETCH_REQUEST') 來告訴中間件我們正在等待一個類型為 FETCH_REQUEST 的 action,然后中間件會暫停執(zhí)行 wacthFetchRequests generator 函數(shù),直到 FETCH_REQUEST action 被 dispatch。一旦我們獲得了匹配的 action,中間件就會恢復(fù)執(zhí)行 generator 函數(shù)。
下一條指令 fork(fetchUrl, action.url) 告訴中間件去無阻塞調(diào)用一個新的 fetchUrl 任務(wù),action.url 作為 fetchUrl 函數(shù)的參數(shù)傳遞。中間件會觸發(fā) fetchUrl generator 并且不會阻塞 watchFetchRequests。當(dāng)fetchUrl 開始執(zhí)行的時候,watchFetchRequests 會繼續(xù)監(jiān)聽其它的 watchFetchRequests actions。當(dāng)然,JavaScript 是單線程的,redux-saga 讓事情看起來是同時進(jìn)行的。
(3)在 worker saga fetchUrl 中,call(fetch,url) 指示中間件去調(diào)用 fetch 函數(shù),同時,會阻塞fetchUrl 的執(zhí)行,中間件會停止 generator 函數(shù),直到 fetch 返回的 Promise 被 resolved(或 rejected),然后才恢復(fù)執(zhí)行 generator 函數(shù)。
另一個栗子:
// example 2
import { takeEvery } from 'redux-saga';
import { call, put } from 'redux-saga/effects';
import axios from 'axios';
// 1. our worker saga
export function* createLessonAsync(action) {
try {
// effects(call, put):
// trigger off the code that we want to call that is asynchronous
// and also dispatched the result from that asynchrous code.
const response = yield call(axios.post, 'http://jsonplaceholder.typicode.com/posts', {section_id: action.sectionId});
yield put({type: 'lunchbox/lessons/CREATE_SUCCEEDED', response: response.data});
} catch(e) {
console.log(e);
}
}
// 2. our watcher saga: spawn a new task on each ACTION
export function* watchCreateLesson() {
// takeEvery:
// listen for certain actions that are going to be dispatched and take them and run through our worker saga.
yield takeEvery('lunchbox/lessons/CREATE', createLessonAsync);
}
// 3. our root saga: single entry point to start our sagas at once
export default function* rootSaga() {
// combine all of our sagas that we create
// and we want to provide all our Watchers sagas
yield watchCreateLesson()
}
最后,總結(jié)一下 redux-saga 的優(yōu)點(diǎn):
(1)聲明式 Effects:所有的操作以JavaScript對象的方式被 yield,并被 middleware 執(zhí)行。使得在 saga 內(nèi)部測試變得更加容易,可以通過簡單地遍歷 Generator 并在 yield 后的成功值上面做一個 deepEqual 測試。
(2)高級的異步控制流以及并發(fā)管理:可以使用簡單的同步方式描述異步流,并通過 fork 實(shí)現(xiàn)并發(fā)任務(wù)。
(3)架構(gòu)上的優(yōu)勢:將所有的異步流程控制都移入到了 sagas,UI 組件不用執(zhí)行業(yè)務(wù)邏輯,只需 dispatch action 就行,增強(qiáng)組件復(fù)用性。
4. 附上測試 demo
5. 參考
redux-saga - Saga Middleware for Redux to Handle Side Effects - Interview with Yassine Elouafi
redux-saga 基本概念
Redux: Thunk vs. Saga
從redux-thunk到redux-saga實(shí)踐
React項(xiàng)目小結(jié)系列:項(xiàng)目中redux異步流的選擇
API calls from Redux 系列