redux-saga 實踐總結

mao103 8年前發布 | 49K 次閱讀 Redux 中間件 JavaScript開發

有關 redux-saga 的文章,網絡上早已是汗牛充棟。因此,本篇主要談一談自己的理解,以及實踐中的經驗總結。

眾所周知,redux 大部分的想法,都來自于 elm 。在 elm 和 redux 中,整個應用就是一個純函數。elm 通過在 reducer 中返回一些聲明副作用的 task 來處理異步問題,而 redux 借鑒 koa 的插件機制,用中間件改造 dispatch ,從而誕生了一批通過構造滿足特殊 pattern 條件的 action 來解決副作用的問題。

而 redux-saga 獨辟蹊徑,監聽 action 來執行有副作用的 task,以保持 action 的簡潔性。并且引入了 sagas 的機制和 generator 的特性,讓redux-saga 非常方便地處理復雜異步問題。

有意思的是,redux 借鑒了 elm,但在處理異步問題(副作用問題在前端一般為異步問題)上,借鑒了 koa 中間件的形式,而 redux-saga 卻又去從 elm 取經,借鑒了獨立 task 的形式。但是說到底,redux-saga 是一個 redux 的中間件。這個故事告訴我們,有好的設計不如有強大的擴展性。

redux-saga 本身也有良好的擴展性。比如,易證得,但凡 redux 中間件,都可以用 redux-saga 來重寫。當然了,不是說用了 redux-saga,其它異步中間件就不能用了,只是說不能保證 redux-saga 能恰好和你之前使用的中間件配合良好。

redux-saga 簡介

redux-saga 是一個 redux 中間件,它具有如下特性:

  • 集中處理 redux 副作用問題。

  • 被實現為 generator 。

  • 類 redux-thunk 中間件。

  • watch/worker(監聽->執行) 的工作形式。

對于剛接觸 redux-saga 的同學,可以先來一段簡單的代碼快速了解 redux-saga 諸多特性。

// 類 thunk 的 worker “進程”
function* load() {
  yield put({ type: BEGIN_LOAD_DATA });

  try {
    const result = yield call(fetch, UrlMap.loadData);

    yield put({
      type: LOAD_DATA_SUCCESS,
      payload: result,
    });
  } catch (e) {
    yield put({
      type: LOAD_DATA_ERROR,
      payload: e,
      error: true,
    });
  }
}

function* saga() {
  // 創建一個監聽“進程”
  yield fork(watch(CLICK_LOAD_BUTTON, load))
}

Effects

Effect 是一個 javascript 對象,里面包含描述副作用的信息,可以通過 yield 傳達給 sagaMiddleware 執行

在 redux-saga 世界里,所有的 Effect 都必須被 yield 才會執行,所以有人寫了 eslint-plugin-redux-saga 來檢查是否每個 Effect 都被 yield。并且原則上來說,所有的 yield 后面也只能跟Effect,以保證代碼的易測性。

例如:

yield fetch(UrlMap.fetchData);

應該用 call Effect :

yield call(fetch, UrlMap.fetchData)

從而可以使代碼可測:

assert.deepEqual(iterator.next().value, call(fetch, UrlMap.fetchData))

關于各個 Effect 的具體介紹,文檔已經寫得很詳細了,這里只做簡要介紹。

1、put

作用和 redux 中的 dispatch 相同。

yield put({ type: 'CLICK_BTN' });

2、select

作用和 redux thunk 中的 getState 相同。

const id = yield select(state => state.id);

3、take

等待 redux dispatch 匹配某個 pattern 的 action 。

在這個例子中,先等待一個按鈕點擊的 action ,然后執行按鈕點擊的 saga:

while (true) {
  yield take('CLICK_BUTTON');
  yield fork(clickButtonSaga);
}

再舉一個利用 take 實現 logMiddleware 的例子:

while (true) {
  const action = yield take('*');
  const newState = yield select();

  console.log('received action:', action);
  console.log('state become:', newState);
}

這種監聽一個 action ,然后執行相應任務的方式,在 redux-saga 中非常常用,因此 redux-saga 提供了一個輔助 Effect —— takeEvery ,讓 watch/worker 的代碼更加清晰。

yield takeEvery('*', function* logger(action) {
  const newState = yield select();

  console.log('received action:', action);
  console.log('state become:', newState);
});

4、阻塞調用和無阻塞調用

redux-saga 可以用 fork 和 call 來調用子 saga ,其中 fork 是無阻塞型調用,call 是阻塞型調用。

如果看過 saga 的論文,就知道 saga 是由許多子 saga (或者 subtransaction)組合起來的。fork Effect 和它的字面意思一樣,即創建一個子 saga 。

4.1、fork

下面寫一個倒數的例子,當接收到 BEGIN_COUNT 的 action,則開始倒數,而接收到 STOP_COUNT 的 action, 則停止倒數。

function* count(number) {
  let currNum = number;

  while (currNum >= 0) {
    console.log(currNum--);
    yield delay(1000);
  }
}

function countSaga* () {
  while (true) {
    const { payload: number } = yield take(BEGIN_COUNT);
    const countTaskId = yield fork(count, number);

    yield take(STOP_TASK);
    yield cancel(countTaskId);
  }
}

4.2、call

有阻塞地調用 saga 或者返回 promise 的函數。

同樣寫一個例子:

const project = yield call(fetch, { url: UrlMap.fetchProject });
const members = yield call(fetchMembers, project.id);

另附 redux-saga 文檔:

傳統異步中間件簡介

在介紹 redux-saga 優缺點之前,這里先簡要介紹傳統的 redux 異步中間件,以便和 redux-saga 做比較。對傳統異步中間件已經充分了解的讀者,可以直接跳到 “redux-saga 優缺點分析” 進行閱讀。

1. fetch-middleware

使用redux的前端技術團隊或個人,大多數都有一套自己 fetch-middleware,一來可以封裝異步請求的業務邏輯,避免重復代碼,二來可以寫一些公共的異步請求邏輯,比如異常接口數據采集、接口緩存、接口處理等等。例如 redux-composable-fetch , redux-api-middleware 。

在當前 redux 社區中,fetch-middleware 封裝結果一般如下:

function loadData(id) {
  return {
    url: '/api.json',
    types: [LOADING_ACTION_TYPE, SUCCESS_ACTION_TYPE, SUCCESS_ACTION_TYPE],
    params: {
      id,
    },
  };
}

值得一提的是,大多數 fetch-middleware 都會用到一個小技巧 —— 把最終處理好的 promise 返回出來,以便在 thunk-middleware 中復用,并組織不同異步過程的先后邏輯。

function loadDetailThunk(id) {
  return (dispatch) => {
    // 先請求到 loadData 的結果,再請求 loadDetail
    dispatch(loadData(id)).then(result => {
      const { id: detailId } = result;
      dispatch(loadDetail(detailId));
    });
  };
}

這個技巧在 redux-saga 中也同樣有效。

function* loadDetailSaga(id) {
  const result = yield put.sync(loadData(id));
  const { id: detailId } = result;

  yield put.sync(loadDetail(detailId));
}

2. redux-thunk-middleware

redux 中大量應用了 thunk 的概念,例如 getState 以延遲執行的方式可以始終獲得最新值,redux-thunk 以延遲執行的方式把副作用的責任推卸到用戶身上。

任何異步問題都能在 thunk 中解決。

3. sequence-middleware

sequence-middleware 用于保證 action 依次執行,無論是異步 action 還是普通 aciton ,和 fetch-middleware 配合使用非常方便。

這里可以把每個 action 可以寫成 thunk action,在 thunk 函數內從 store 拿到參數,避免 action 之間的依賴。這樣不管業務邏輯有多復雜,都可以通過用 sequence action 輕易組織。

function loadDetailThunk() {
  return function(dispatch, getState) {
    const detailId = _.get(getState(), `${currPath}.detailId`);

    dispatch({
      url: UrlMap.getDetail,
      params: { detailId },
    });
  };
}

function loadDetail() {
  return [loadData(), loadDetailThunk()];
}

redux-saga 優缺點分析

缺點

  • redux-saga 不強迫我們捕獲異常,這往往會造成異常發生時難以發現原因。因此,一個良好的習慣是,相信任何一個過程都有可能發生異常。如果出現異常但沒有被捕獲,redux-saga 的錯誤棧會給你一種一臉懵逼的感覺。

  • generator 的調試環境比較糟糕,babel 的 source-map 經常錯位,經常要手動加 debugger 來調試。

  • 你團隊中使用的其它異步中間件,或許難以和 redux-saga 搭配良好。或許需要花費一些代價,用 redux-saga 來重構一部分中間件。

優點

  • 保持 action 的簡單純粹,aciton 不再像原來那樣五花八門,讓人眼花繚亂。task 的模式使代碼更加清晰。

  • redux-saga 提供了豐富的 Effects,以及 sagas 的機制(所有的 saga 都可以被中斷),在處理復雜的異步問題上十分趁手。如果你的應用屬于寫操作密集型或者業務邏輯復雜,快讓 redux-saga 來拯救你。

  • 擴展性強。

  • 聲明式的 Effects,使代碼更易測試。

利用 redux-saga 寫 redux 中間件

用 redux-saga 來寫中間件,可謂事半功倍。這里舉一個輪詢中間件的例子。

function* pollingSaga(fetchAction) {
  const { defaultInterval, mockInterval } = fetchAction;

  while (true) {
    try {
      const result = yield put.sync(fetchAction);
      const interval = mockInterval || result.interval;

      yield delay(interval * 1000);
    } catch (e) {
      yield delay(defaultInterval * 1000);
    }
  }
}

function* beginPolling(pollingAction) {
  const { pollingUrl, defaultInterval = 300, mockInterval, types,
    params = {} } = pollingAction;

  if (!types[1]) {
    console.error('pollingAction pattern error', pollingAction);
    throw Error('pollingAction types[1] is null');
  }

  const fetchAction = {
    url: pollingUrl,
    types,
    params,
    mockInterval,
    defaultInterval,
  };

  const pollingTaskId = yield fork(pollingSaga, fetchAction);
  const pattern = action => action.type === types[1] && action.stopPolling;

  yield take(pattern);
  yield cancel(pollingTaskId);
}

function* pollingSagaMiddleware() {
  yield takeEvery(action => {
    const { pollingUrl, types } = action;

    return pollingUrl && types && types.length;
  }, beginPolling);
};

最后,redux-saga 在實踐的沉淀,我已經總結到 redux-saga-sugar .

 

來自:https://zhuanlan.zhihu.com/p/23012870

 

 本文由用戶 mao103 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!