[譯] Don’t call me, I’ll call you:使用 Redux-Saga 管理 React 應用中的非同步 action (上)

jonjia發表於2018-04-02

Don’t call me, I’ll call you:使用 Redux-Saga 管理 React 應用中的非同步 action (上)

[譯] Don’t call me, I’ll call you:使用 Redux-Saga 管理 React 應用中的非同步 action (上)

在接下來的兩篇文章中,我想談談在 React 應用中使用 Redux-Saga 進行非同步 action 管理的基礎和進階方法。我會說明為什麼我們會在 AppsFlyer 專案中使用它,以及它可以解決什麼問題。

本篇文章主要介紹 Redux-Saga 相關的基本概念,下篇專門討論 Redux-Saga 可以解決哪些問題。請注意:閱讀這兩篇文章,你要對 ReactRedux 有一定的瞭解。

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 屬性的值就是 undefineddone 屬性的值就是 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 effecttake 方法,這個方法會阻塞 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 被觸發。下次重新進入迴圈後,也會重複上述過程。

讓我們一步步地看一下上面的過程:

  1. 應用啟動,執行所有 Sagas。
  2. mySaga 執行,進入 while(true) 迴圈,在第 3 行掛起。
  3. USER_INTERACTED_WITH_UI_ACTION 這個 action 被觸發。
  4. Saga 的執行緒啟用,執行第 4 行,觸發 SHOW_LOADING_ACTION 這個 action,然後分配的 reducer 進行處理(reducer 處理後,頁面就會顯示 loading 提示)。
  5. 傳送一個請求到服務端(第 5 行),然後會再次掛起,直到請求的 Promise 變為 resolved,請求結果的資料會賦值給 data 變數。
  6. SHOW_DATA_ACTION 接收 data 作為引數被觸發,然後 reducer 就可以使用這些資料來更新頁面。
  7. 再次進入迴圈,回到第 2 步。

接下來

在這篇文章中,我們介紹了 Redux-Saga 相關的基本概念,展示瞭如何在 React 應用中使用它。下篇文章中,我會展示在實際應用中使用它獲得的價值。

感謝 Yotam KadishayLiron Cohen


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章