原文請見.
saga其實也是一種設計模式, 他的作用是wrapper包裝器的原理,現在我手頭沒有javascirpt設計模式的書,不知道有沒有這個模式.簡單說Saga就是把邏輯進行了打包,
這樣在書寫邏輯關系的時候就相對比較容易了
.所有有關的邏輯卸載一起,符合組件分離,集中控制的原理.(我們的電腦就是這么搞的, 外界設備很多,但是控制權在操作系統上,cpu其實就是我們的saga,用來處理操作流程的)
開始
幾天前我的同事做了一個關于異步操作管理的講座.他正在使用幾個工具來擴展Redux的功能.聽了他的報告,簡直讓我產生了javascirpt 疲勞.
讓我們面對事實:如果你習慣于基于需求來使用技術完成工作-并不是從技術本身來考慮問題的話-構建React 生態系統將會是非常令人沮喪和花費時間的.(譯注:高屋建瓴的話,細細體會:意思別光顧眼前的項目,要有長遠的開發眼光).
我已經在Angular 項目上花費了兩年時間,非常的喜歡MVC模型對于state控制的能力.但是我要說即使我已經有了Backbone.js(譯注:另一個前端MVC框架)
的背景,但是Angular.js的學習曲線仍然很有挑戰性.我因此獲得了不錯的工作,因此我也有機會在有關的項目上使用它。我從Angular的舍去學習到了很多的東西.
還挺酷的,但是Fatigue還是在繼續(譯注:Angular的水很深啊,吃不透的意思)
所以我遷移到了時髦的框架上:React,Redux,Sagas.
幾年以前,我遇到一篇文章,是關于扁平promise對象鏈的文章.我從這篇文章上學到了很多東西。甚至是兩年以后,我仍然能回想起文章里的真知灼見.
申明:我將乎繼續使用一樣的場景,并擴展他.我希望能創建相同方法的討論.我會假設讀者已經對于Promise對象,React,Redux有了基礎的了解,當然還有JavaScript(基礎的基礎).
首先要做的事情
根據Redux-saga的創建者Yassine Flouafi的想法:
redux-saga是一個在React/Redux應用中,針對性解決異步操作的庫.
基本上,他是一個助手庫,基于Sagas和ES6的Generators函數來進行組織異步和分發操作.如果你想進了解Saga的模式,可以參見Caitie McCattrey的視頻.
航班展示案例
場景是這樣的

如上面的圖所示,三個API的調用時一個承前啟后的過程,getDeparture->getFlight->getForecast,所以我們的API Serivces類看起來是這樣的:
class TravelServiceApi {//旅行服務API
static getUser() { //首先異步獲取用戶信息
return new Promise((resolve) => {
setTimeout(() => {
resolve({//模擬返回的數據
email : "somemockemail@email.com",
repository: "http://github.com/username"
});
}, 3000);
});
}
static getDeparture(user) {//獲取用戶航班信息
return new Promise((resolve) => {
setTimeout(() => {
resolve({//模擬返回的數據
userID : user.email,
flightID : “AR1973”,
date : “10/27/2016 16:00PM”
});
}, 2500);
});
}
static getForecast(date) {//獲取天氣情況
return new Promise((resolve) => {
setTimeout(() => {
resolve({ //模擬返回的數據
date: date,
forecast: "rain"
});
}, 2000);
});
}
}
這個API非常的直接,允許我們使用假數據來設定場景.首先我們的需要一個用戶,然后根據用戶的的信息,來獲取用戶的航班信息,我們得到離港信息,航班信息和天氣信息,據此我們可以創建幾個丑陋的儀表盤

使用的React組件可以看這里.這是三個不同過的組件,數據來源是redux store中的三個reducer.類似下面的樣子:
const dashboard = (state = {}, action) => {
switch(action.type) {
case ‘FETCH_DASHBOARD_SUCCESS’:
return Object.assign({}, state, action.payload);
default :
return state;
}
};
因為有三個不同的場景,我們為每一個面板使用一個不同的reducer,這么做就可以讓組件從Redux的StateProps函數獲取到需要的部分:
const mapStateToProps =(state) => ({
user : state.user,
dashboard : state.dashboard
});
每個步驟都配置好以后(我很清楚,很多的細節問題都還沒有解釋,但是我想集中注意在sagas上),準備開始運行了.
展現我們的Sagas
William Deming 曾經說過:
如果你不能描述出你所做事情的步驟,那么你就不知道你到底在做什么
(譯注:process或者flow對于Redux的學習很重要,初學者總是不能把分散的一些部分連城一個整體去考慮).
那么,我們就來一步一步的使用Redux Saga創建我們的工作流程.
1.注冊Sagas
我將會使用我自己的表達方式來描述API 暴露出來的方法.如果你需要更多的技術細節,參考文檔
首先我們需要創建saga 生成器函數(譯注:generator,這算是坎吧,要先理解ES6的內容),并且注冊一下.
function* rootSaga() {
yield[
fork(loadUser),
takeLatest('LOAD_DASHBOARD', loadDashboardSequenced)
];
}
Redux saga暴露出了幾個方法,這幾個方法被成為effects
,我們將會定義幾個effects:
Fork 非阻塞執行傳遞進來的函數(譯注:這算是有一個坎兒,javascript的函數是一類對象,可以作為函數的參數來傳遞,初學者很難理解這個問題)
Take 暫停執行直達收到匹配的action
Race 同時運行effects,然后其中之一完成了,就會立即退出,其他的effects也就會終止
Call 執行一個函數,如果他返回一個promise對象(譯注:異步操作的又一個坎兒),saga就會在這里終止,知道promise返回 resolved
put dispatch一個動作
Select 運行selector函數從state獲取數據
takeLatest 意思是我們將會執行一個操作,操作只會返回最新的一個滴啊用的結果.如果我們出發幾個cases,將會忽略所有的操作,除了最后一個.
takeEvery將會返回每個調用的結果
我們將會注冊兩個不同的sagas.稍后會定義他們.目前我們為了user信息使用了fork和takeLeatest,他們會等待直到”LOAD_DASBOARD“的調用后,才會執行.
2.把Saga中間件注入到Redux的store中
當我們定義了Redux store并初始化以后,大多數情況下看起來像這樣:
const sagaMiddleware = createSagaMiddleware();
const store = createStore(rootReducer, [], compose(
applyMiddleware(sagaMiddleware)
);
sagaMiddleware.run(rootSaga); /* inject our sagas into the middleware*/
3.創建Sagas
首先,我們將定義loadUser的流程 saga:
function* loadUser() {
try {
//1st step
const user = yield call(getUser);
//2nd step
yield put({type: 'FETCH_USER_SUCCESS', payload: user});
} catch(error) {
yield put({type: 'FETCH_FAILED', error});
}
}
我按照下面的方式解讀一下:
- 首先調用getUser函數,返回的結果賦值給user常量
- 之后,dispatch一個叫做FETCH_USER_SUCCESS的action,user傳遞給store去處理.
- 如果操作中出問題了,dispatch一個FETCH_FAILED的 action
如你所見的,的確是非常的酷,我們把yield操作的結果賦值給了一個變量
接著來創建saga序列
function* loadDashboardSequenced() {
try {
yield take(‘FETCH_USER_SUCCESS’);
const user = yield select(state => state.user);
const departure = yield call(loadDeparture, user);
const flight = yield call(loadFlight, departure.flightID);
const forecast = yield call(loadForecast, departure.date);
yield put({type: ‘FETCH_DASHBOARD_SUCCESS’, payload: {forecast, flight, departure} });
} catch(error) {
yield put({type: ‘FETCH_FAILED’, error: error.message});
}
}
按照下面的步驟來解讀:
等待
FETCH_USER_SUCCESS
action的被派發,這個操作的基礎是基于一個事件被暫定直到觸發為止.我們使用take effect來實施這個過程我們從store中獲取一個值.select effects接收一個函數可以接入到store.我們把用戶信息賦值給user常量
執行一個異步操作來加載depature信息,使用call effect ,user常量作為參數
loadDeparture 完成以后,我們執行 loadFlight,參數是前一個操作異步獲取的departure對象.
forecast的操作是一樣的,我們需要等待航班信息加載完成以后才可以執行下一個call effect
最后,當所有的操作都完成以后,使用put effect來分發一個action,把所有獲取的信息發送到Redux的store.
正如你看到的,一個saga就是一系列等待前一個action來修改他們行為的集合體(譯注:集合中每個步驟都會等待前一個步驟的操作結果).一旦完成整個流程,所有的信息就可以提供給Redux的store來處理了.
是不是相當的整潔啊!
接下來,我們看看一個不同的示例.考慮一下 getFlight 和 getForecast 同時觸發,他們不需要一個等另一個完成了再執行下一個.
所以我們可以創建一個不同的面板

非阻塞Saga
為了執行兩個非阻塞操作,需要對之前的saga做一點改動:
function* loadDashboardNonSequenced() {
try {
//Wait for the user to be loaded
yield take('FETCH_USER_SUCCESS');
//Take the user info from the store
const user = yield select(getUserFromState);
//Get Departure information
const departure = yield call(loadDeparture, user);
//Here is when the magic happens
const [flight, forecast] = yield [call(loadFlight, departure.flightID), call(loadForecast, departure.date)];
//Tell the store we are ready to be displayed
yield put({type: 'FETCH_DASHBOARD2_SUCCESS', payload: {departure, flight, forecast}});
} catch(error) {
yield put({type: 'FETCH_FAILED', error: error.message});
}
}
我們把yield注冊為一個數組:
const [flight, forecast] = yield [call(loadFlight, departure.flightID), call(loadForecast, departure.date)];
因此,兩個操作會同時被調用(并行),但是在在最后們會等待兩個操作都返回結果以后,如果有需求再更新UI
接著我們在rootSaga中注冊saga
function* rootSaga() {
yield[
fork(loadUser),
takeLatest('LOAD_DASHBOARD', loadDashboardSequenced),
takeLatest('LOAD_DASHBOARD2' loadDashboardNonSequenced)
];
}
如果操作一旦完成就需要更新UI,應該怎么辦?
別擔心,我們往回看一下
非序列和非阻塞Sagas
我們可以隔離我們的saga,也可以合并他們,意思是saga可以獨立的工作.這就是我們需要的操作.看看操作步驟
step #1 把Forecast和Flight Saga隔離開,這兩個Saga都依賴departure的操作
/* **************Flight Saga************** */
function* isolatedFlight() {
try {
/* departure will take the value of the object passed by the put*/
const departure = yield take('FETCH_DEPARTURE3_SUCCESS');
const flight = yield call(loadFlight, departure.flightID);
yield put({type: 'FETCH_DASHBOARD3_SUCCESS', payload: {flight}});
} catch (error) {
yield put({type: 'FETCH_FAILED', error: error.message});
}
}
/* **************Forecast Saga************** */
function* isolatedForecast() {
try {
/* departure will take the value of the object passed by the put*/
const departure = yield take('FETCH_DEPARTURE3_SUCCESS');
const forecast = yield call(loadForecast, departure.date);
yield put({type: 'FETCH_DASHBOARD3_SUCCESS', payload: { forecast, }});
} catch(error) {
yield put({type: 'FETCH_FAILED', error: error.message});
}
}
有什么要注意的嗎?這是我們的構建過程
- 兩個saga都等待同一個 Action 事件(FETCH_DEPARTURE3_SUCCESS)的結果,來執行后續的工作.
- 當這個事件被觸發的時候,他們會收到一個值,這個問題的細節,下一步會講.
- 兩個saga使用call effect來執行異步操作,異步操作完成以后,他們會觸發同一個事件.但是發送到store的數據是不同的.感謝Redux的巨大威力,我們這樣操作,但是不會對reducer做任何修改.
step #2 對departure序列做出改動,發送一個departure值到兩個其他的saga:
function* loadDashboardNonSequencedNonBlocking() {
try {
//Wait for the action to start
yield take('FETCH_USER_SUCCESS');
//Take the user info from the store
const user = yield select(getUserFromState);
//Get Departure information
const departure = yield call(loadDeparture, user);
//Update the store so the UI get updated
yield put({type: 'FETCH_DASHBOARD3_SUCCESS', payload: { departure, }});
//trigger actions for Forecast and Flight to start...
//We can pass and object into the put statement
yield put({type: 'FETCH_DEPARTURE3_SUCCESS', departure});
} catch(error) {
yield put({type: 'FETCH_FAILED', error: error.message});
}
}
之前的代碼沒有變化,put effect這里有改變.我們可以給一個action傳遞一個對象,它將會把yield的結果賦值給一個departure常量,departure saga和flight saga都這樣操作.
看看demo,注意一下第三個面板加載的foreacast要比flight快一點,因為flight的延時長了一點,模擬了慢速的請求過程.
在實際生產的app中,處理的過程可能有一點不一樣.我只是想說明在使用put effect的時候怎么傳遞值.
關于測試?
你會測試你的代碼,對不?
Saga很容易測試,因為saga耦合了操作步驟,根據生成器函數的邏輯,操作步驟被序列化了.讓我們看看實例:
describe('Sequenced Saga', () => {
const saga = loadDashboardSequenced();
let output = null;
it('should take fetch users success', () => {
output = saga.next().value;
let expected = take('FETCH_USER_SUCCESS');
expect(output).toEqual(expected);
});
it('should select the state from store', () => {
output = saga.next().value;
let expected = select(getUserFromState);
expect(output).toEqual(expected);
});
it('should call LoadDeparture with the user obj', (done) => {
output = saga.next(user).value;
let expected = call(loadDeparture, user);
done();
expect(output).toEqual(expected);
});
it('should Load the flight with the flightId', (done) => {
let output = saga.next(departure).value;
let expected = call(loadFlight, departure.flightID);
done();
expect(output).toEqual(expected);
});
it('should load the forecast with the departure date', (done) => {
output = saga.next(flight).value;
let expected = call(loadForecast, departure.date);
done();
expect(output).toEqual(expected);
});
it('should put Fetch dashboard success', (done) => {
output = saga.next(forecast, departure, flight ).value;
let expected = put({type: 'FETCH_DASHBOARD_SUCCESS', payload: {forecast, flight, departure}});
const finished = saga.next().done;
done();
expect(finished).toEqual(true);
expect(output).toEqual(expected);
});
});
- 確保導入所有需要測試的effect和助手函數
- 當你在一個yield中存儲一個值時,需要傳遞一個mock 數據 到下一個函數.注意看看第三個,第四個和第五個測試
- 在測試的幕后,當下一個方法被調用以后,yield完成以后,就會移動到下一步.這就是為什么我們使用saga.next().value的原因.
- 序列會被固話,如果你改變saga的步驟,測試就不會通過了.
結論
我非常喜歡測試新技術.每天都會返現新東西.這就像時裝:一旦一些事情被公眾接受,好像每個人都想使用它.有時候我會在這些事情中發現一些價值(譯注:意思是別人說好,你接受了,也可以獲得很多有意義的東西,但是不是全部),但是坐下來考慮一下我們正真需要什么是非常重要的.
我發現thunks更容易實現和維護,但是對于復雜的操作,Redux-saga能做的更好.
再次聲明,感謝 Thomas 為這篇文章提供的靈感.我希望其他人也可以從我的文章中激發一些靈感:)
如果你閱讀中有問題,可以tweet我.我樂意提供幫助.
翻譯結束. 好文章.知乎好像有也有一篇翻譯稿,才看到.如果有興趣可以參考一下