Don’t call me, I’ll call you:使用 Redux-Saga 管理 React 應用中的非同步 action (上)
在接下來的兩篇文章中,我想談談在 React 應用中使用 Redux-Saga 進行非同步 action 管理的基礎和進階方法。我會說明為什麼我們會在 AppsFlyer 專案中使用它,以及它可以解決什麼問題。
本篇文章主要介紹 Redux-Saga 相關的基本概念,下篇專門討論 Redux-Saga 可以解決哪些問題。請注意:閱讀這兩篇文章,你要對 React 和 Redux 有一定的瞭解。
Generators 先行!
為了理解 Sagas,我們首先要理解什麼是 Generator。下面是 MDN 對 Generator 的描述:
Generator 是在執行時能暫停,後面又能從暫停處繼續執行的函式。它的上下文會在繼續執行時儲存。
你可以把 Generator 理解成一種遍歷器物件生成函式,(譯註:Generator 執行後返回的遍歷器物件)提供一個 next
方法。執行這個方法就會返回下一個狀態,或者返回遍歷結束的狀態。這就需要 Generator 能夠維護內部狀態。
下面是一個基本的 Generator 示例,它生成的遍歷器物件會返回幾個字串:
function* namesEmitter() {
yield "William";
yield "Jacob";
return "Daniel";
}
// 執行 Generator
var generator = namesEmitter();
console.log(generator.next()); // prints {value: "William", done: false}
console.log(generator.next()); // prints {value: "Jacob", done: false}
console.log(generator.next()); // prints {value: "Daniel", done: true}
複製程式碼
next
方法的返回值結構非常簡單 — 只要我們通過 yield/return
返回值,這個返回值就是 value
屬性的值。如果我們沒有返回值,value
屬性的值就是 undefined,done
屬性的值就是 true
。
還有一點值的注意的是,執行 namesEmitter
後,函式會在呼叫 yield
的地方停下來。當我們呼叫 next
方法後,函式會繼續執行,直到遇到下一個 yield
。如果我們呼叫了 return
語句或者函式執行完畢,done
屬性就會為真。
如果狀態序列的長度不確定時,我們可以用下面的方法來寫:
var results = generator.next();
while(!results.done){
console.log(results.value);
results = generator.next();
}
console.log(results.value);
複製程式碼
什麼是 Sagas?
Sagas 是通過 Generator 函式來建立的。官方文件 的解釋如下:
Saga 就像應用中的一個獨立執行緒,完全負責管理非同步 action。
你可以把 Saga 想象成一個以最快速度不斷地呼叫 next
方法並嘗試獲取所有 yield
表示式值的執行緒。你可能會問這和 React 有什麼關係,為什麼要使用它,所以首先來看看如何在 React & Redux 應用使用 Saga:
在 React & Redux 應用中,一個常見的用法從呼叫一個 action 開始。被分配用來處理這個 action 的 reducer 會使用新的 state 更新 store,隨後檢視就會被更新渲染。 如果一個 Saga 被分配用來處理這個 action — 這個 action 通常就是個非同步 action(比如一個對服務端的請求),一旦這個 action 完成後,Saga 會呼叫另一個 action 讓 reducer 進行處理。
常見用例
我們可以通過一個常見流程來說明: 使用者與頁面進行互動,這個互動動作會觸發一個從服務端請求資料的動作(此時頁面顯示 loading 提示),最終我們用請求回來的資料去渲染頁面的內容。 讓我們為每步建立一個 action,然後用 Redux-Saga 實現一個簡化的版本如下:
// saga.js
import { take } from 'redux-saga/effects'
function* mySaga(){
yield take(USER_INTERACTED_WITH_UI_ACTION);
}
複製程式碼
這個 Saga 的函式名叫做 mySaga
。它呼叫了 Redux-Saga effect 的 take
方法,這個方法會阻塞 Saga 的執行,直到有人呼叫了作為引數的那個 action,Saga 的執行也會結束,就像我們前面看到的 Generator 一樣(done 變為 true)。
現在我們要讓頁面展示 loading 提示來響應這個 action。可以通過 put
方法呼叫另一個 action,然後分配 reducer 來處理,從而完成上述功能。如下:
// saga.js
import { take, put } from 'redux-saga/effects'
function* mySaga(){
yield take(USER_INTERACTED_WITH_UI_ACTION);
yield put(SHOW_LOADING_ACTION, {isLoading: true});
}
// reducer.js
...
case SHOW_LOADING_ACTION: (state, isLoading) => {
return Object.assign({}, state, {showLoading: isLoading});
}
...
複製程式碼
下一步是呼叫 call
方法,它接收一個函式和一組引數,使用這些引數來執行這個函式。我們給 call
方法傳遞一個請求服務端並返回一個 Promise 的 GET
函式,它會儲存請求結果:
// saga.js
import { take, put, call } from 'redux-saga/effects'
function* mySaga(){
yield take(USER_INTERACTED_WITH_UI_ACTION);
yield put(SHOW_LOADING_ACTION, {isLoading: true});
const data = yield call(GET, 'https://my.server.com/getdata');
yield put(SHOW_DATA_ACTION, {data: data});
}
// reducer.js
...
case SHOW_DATA_ACTION: (state, data) => {
return Object.assign({}, state, {data: data, showLoading: false};
}
...
複製程式碼
通過呼叫 SHOW_DATA_ACTION 來用接收的資料更新頁面。
剛剛發生了什麼?
應用啟動後,所有的 Sagas 都會被執行,你可以認為一直在呼叫 next
方法直到結束。take
方法類似於執行緒掛起的作用,一旦呼叫了USER_INTERACTED_WITH_UI_ACTION,執行緒就會恢復執行。
然後,我們繼續呼叫 SHOW_LOADING_ACTION,reducer 會處理這個 action。由於 Saga 還在繼續執行,call
方法會發起對服務端的請求,Saga 會在再次掛起,直到請求結束。
每次都使用
在上面的例子中,Saga 只處理了一個使用者互動的 action,因為我們用 put
方法執行了 SHOW_DATA_ACTION
這個 action,然後後面就沒有 yield 了(done 就是 true 了對吧?)。
如果我們希望在每次呼叫 USER_INTERACTED_WITH_UI_ACTION
這個 action 的時候,都會執行這一系列的 actions,我們可以用 while(true)
語句來包裹 Saga 內部的邏輯程式碼。完整程式碼如下:
// saga.js
import { take, put, call } from 'redux-saga/effects'
1. function* mySaga(){
2. while (true){
3. yield take(USER_INTERACTED_WITH_UI_ACTION);
4. yield put(SHOW_LOADING_ACTION, {isLoading: true});
5. const data = yield call(GET, 'https://my.server.com/getdata');
6. yield put(SHOW_DATA_ACTION, {data: data});
7. }
8. }
// reducer.js
...
case SHOW_LOADING_ACTION: (state, isLoading) => {
return Object.assign({}, state, {showLoading: isLoading});
},
case SHOW_DATA_ACTION: (state, data) => {
return Object.assign({}, state, {data: data, showLoading: false};
}
...
複製程式碼
這個無限迴圈不會造成堆疊溢位,也不會使你的應用崩潰!因為 take
方法就像執行緒掛起一樣,mySaga
執行後會一直保持 pending
狀態,直到那個 action 被觸發。下次重新進入迴圈後,也會重複上述過程。
讓我們一步步地看一下上面的過程:
- 應用啟動,執行所有 Sagas。
- mySaga 執行,進入
while(true)
迴圈,在第 3 行掛起。 USER_INTERACTED_WITH_UI_ACTION
這個 action 被觸發。- Saga 的執行緒啟用,執行第 4 行,觸發
SHOW_LOADING_ACTION
這個 action,然後分配的 reducer 進行處理(reducer 處理後,頁面就會顯示 loading 提示)。 - 傳送一個請求到服務端(第 5 行),然後會再次掛起,直到請求的 Promise 變為 resolved,請求結果的資料會賦值給 data 變數。
SHOW_DATA_ACTION
接收 data 作為引數被觸發,然後 reducer 就可以使用這些資料來更新頁面。- 再次進入迴圈,回到第 2 步。
接下來
在這篇文章中,我們介紹了 Redux-Saga 相關的基本概念,展示瞭如何在 React 應用中使用它。下篇文章中,我會展示在實際應用中使用它獲得的價值。
感謝 Yotam Kadishay 和 Liron Cohen。
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。