Redux-Saga 初識和總結

美團點評點餐發表於2019-03-04

作者介紹:羅雪婧,美團點評前端工程師,3年 Web 前端開發經驗,現在是美團點評點餐團隊的一員。

一、Redux-Saga介紹

redux-saga 是一個旨在於在React/Redux應用中更好、更易地解決非同步操作(action)的庫。主要模組是 saga 會像一個分散的支線在你的應用中單獨負責解決非同步的action(類似於後臺執行的程式)。詳細移步:Redux-saga

redux-saga相當於在Redux原有資料流中多了一層,對Action進行監聽,捕獲到監聽的Action後可以派生一個新的任務對state進行維護(當然也不是必須要改變State,可以根據專案的需求設計),通過更改的state驅動View的變更。圖如下所示:

Redux-Saga 初識和總結

用過redux-thunk的人會發現,redux-saga 其實和redux-thunk做的事情類似,都是可以處理非同步操作和協調複雜的dispatch。不同點在於:

  • Sagas 是通過 Generator 函式來建立的,意味著可以用同步的方式寫非同步的程式碼;
  • Thunks 是在 action 被建立時才呼叫,Sagas 在應用啟動時就開始呼叫,監聽action 並做相應處理; (通過建立 Sagas 將所有的非同步操作邏輯收集在一個地方集中處理)
  • 啟動的任務可以在任何時候通過手動取消,也可以把任務和其他的 Effects 放到 race 方法裡可以自動取消;

二、入門demo

redux-saga-beginner-tutorial

$ git clone https://github.com/HelianXJ/redux-saga-beginner-tutorial.git
$ git checkout redux-tool-saga // 切到有redux tool的分支配合chorme 的 Redux DevTools 工具檢視邏輯更清晰

$ npm i  //下載依賴
$ npm run hello //先看專案檔案中的hello sagas複製程式碼

啟動server成功後view-on: http://172.22.32.14:9966/

可看到如下介面,一個簡單的例子,點選say hello按鈕展示 hello,點選say goodbye按鈕展示goodbye。可注意看右邊欄的Action變化和console控制檯的輸出。

Redux-Saga 初識和總結

Redux-Saga 初識和總結

sagas.js 關鍵程式碼

import { takeEvery } from `redux-saga`;

export function* helloSaga() {
  console.log(`Hello Sagas!`);
}

export default function* watchIncrementAsync() {
    yield* takeEvery(`SAY_HELLO`, helloSaga);
}複製程式碼

這裡sagas建立了一個watchIncrementAsync 監聽SAY_HELLO的Action,派生一個新的任務——在控制檯列印出“Hello Sagas!”通過這例子可以理解redux-saga大致做的事情。

該專案中還有一個計數器的簡單例子。

$ npm start //即可檢視Counter的例子複製程式碼

sagas.js關鍵程式碼

// 一個工具函式:返回一個 Promise,這個 Promise 將在 1 秒後 resolve
export const delay = ms => new Promise(resolve => setTimeout(resolve, ms))

// Our worker Saga: 將非同步執行 increment 任務
export function* incrementAsync() {
    yield delay(1000);
      yield put({ type: `INCREMENT` });
}

// Our watcher Saga: 在每個 INCREMENT_ASYNC action 呼叫後,派生一個新的 incrementAsync 任務
export default function* watchIncrementAsync() {
      yield* takeEvery(`INCREMENT_ASYNC`, incrementAsync);
}複製程式碼

計數器例子的單元測試 sagas.spec.js 關鍵程式碼

import test from `tape`;
import { put, call } from `redux-saga/effects`
import { incrementAsync, delay } from `./sagas`

test(`incrementAsync Saga test`, (assert) => {
  const gen = incrementAsync()

  assert.deepEqual(
    gen.next().value,
    call(delay, 1000),
    `incrementAsync Saga must call delay(1000)`
  )

  assert.deepEqual(
    gen.next().value,
    put({type: `INCREMENT`}),
    `incrementAsync Saga must dispatch an INCREMENT action`
  )

  assert.deepEqual(
    gen.next(),
    { done: true, value: undefined },
    `incrementAsync Saga must be done`
  )

  assert.end()
});複製程式碼

由於redux-saga是用ES6的Generators實現非同步,incrementAsync 是一個 Generator 函式,所以當我們在 middleware 之外執行它,會返回一個易預見的遍歷器物件, 這一點應用在單元測試中更容易寫unit。

redux-saga能做的不只是可以做以上例子的事情。

實際上 redux-saga 所有的任務都通用 yield Effects 來完成。它為各項任務提供了各種 Effect 建立器,可以是:

  • 呼叫一個非同步函式;
  • 發起一個 action 到 Store;
  • 啟動一個後臺任務或者等待一個滿足某些條件的未來的 action。

三、redux-sagas的使用

  • 組合sagas (yield Sagas) —— 實際上和redux-thunk 的dispatch 一個action類似
function* fetchPosts() {
  yield put( actions.requestPosts() )
  const products = yield call(fetchApi, `/products`)
  yield put( actions.receivePosts(products) )
}

function* watchFetch() {
  while ( yield take(FETCH_POSTS) ) {
    yield call(fetchPosts) // waits for the fetchPosts task to terminate
  }
}複製程式碼

當 yield 一個 call 至 Generator,Saga 將等待 Generator 處理結束, 然後以返回的值恢復執行

  • 任務取消 —— 一旦任務被 fork,可以使用 yield cancel(task) 來中止任務執行。取消正在執行的任務,將丟擲 SagaCancellationException 錯誤。

Redux-Saga 初識和總結

  • 同時執行多個任務
const [users, repos] = yield [
     call(fetch, `/users`),
     call(fetch, `/repos`)
 ]複製程式碼
  • 使用輔助函式管理 Effects 之間的併發。
function* takeEvery(pattern, saga, ...args) {
  while(true) const action = yield take(pattern)
    yield fork(saga, ...args.concat(action))
  }
}複製程式碼

三、Redux-Saga優點

  • 流程拆分更細,非同步的action 以及特殊要求的action(更復雜的action)都在sagas中做統一處理,流程邏輯更清晰,模組更乾淨;
  • 以用同步的方式寫非同步程式碼,可以做一些async 函式做不到的事情 (無阻塞併發、取消請求)
  • 能容易地測試 Generator 裡所有的業務邏輯
  • 可以通過監聽Action 來進行前端的打點日誌記錄,減少侵入式打點對程式碼的侵入程度

四、帶來的問題和可接受性

  • action 任務拆分更細,原有流程上相當於多了一個環節。對開發者的設計和抽象拆分能力更有要求,程式碼複雜性也有所增加。
  • 非同步請求相關的問題較難除錯排查