redux-saga 瞭解一下

最近項目用了dva,dva對於異步action的處理是用了redux-saga,故簡單學習了下redux-saga; 如下從 是什麼 爲何 怎麼用 三方面來了解。javascript

是什麼

redux-saga 就是 redux 的一個中間件,用於更優雅地管理反作用(side effects);html

redux-saga能夠理解爲一個 和 系統交互的 常駐進程,可簡單定義: saga = Worker + Warcher前端

名詞解釋

  • side effects

Side effects are the most common way that a program interacts with the outside world (people, filesystems, other computers on networks) [from Wikipedia]java

反作用是 程序與外部世界(人、文件系統,網絡上的其餘計算機) 交互的最經常使用的方式。 映射到前端, 反作用通常指異步網絡請求。git

  • Effect

An effect is a plain JavaScript Object containing some instructions to be executed by the saga middleware.github

effect 是一個普通的 javascript對象,包含一些指令,這些指令最終會被 redux-saga 中間件 解釋並執行。redux

在 redux-saga 世界裏,全部的 Effect 都必須被 yield 纔會執行後端

原則上來講,全部的 yield 後面也只能跟Effect,以保證代碼的易測性。 eg:api

yield fetch(UrlMap.fetchData);數組

應該用 call Effect : yield call(fetch, UrlMap.fetchData)

  • task task 是 generator 方法的執行環境,全部saga的generator方法都跑在task裏。

爲何

做用

用於更優雅地管理反作用, 在前端就是異步網絡請求;本質就是爲了解決異步action的問題;

優勢
  • 反作用轉移到單獨的saga.js中,再也不摻雜在action.js中,保持 action 的簡單純粹,又使得異步操做集中能夠被集中處理。對比redux-thunk

  • redux-saga 提供了豐富的 Effects,以及 sagas 的機制(全部的 saga 均可以被中斷),在處理複雜的異步問題上更順手。提供了更加細膩的控制流

  • 對比thunk,dispatch 的參數依然是一個純粹的 action (FSA)

  • 每個 saga 都是 一個 generator function,代碼能夠採用 同步書寫 的方式 去處理 異步邏輯(No Callback Hell),代碼變得更易讀。

  • 一樣是受益於 generator function 的 saga 實現,代碼異常/請求失敗 均可以直接經過 try/catch 語法直接捕獲處理。

怎麼用

hello saga

  1. 單獨的文件:sagas.js, 統一管理反作用:
export function* helloSaga() {
   console.log('Hello Sagas!');
}
複製代碼
  1. 將saga和store關聯起來, 入口文件 main.js:
import { createStore, applyMiddleware } from 'redux';
 import createSagaMiddleware from 'redux-saga';

 import helloSaga from './sagas'
 import rootReducer from './reducers'

 // 建立 saga middleware
  const sagaMiddleware  = createSagaMiddleware();

 // 建立 store
 const store = createStore(
   rootReducer,
   applyMiddleware(sagaMiddleware)   // 注入 saga middleware
 );

 // 啓動 saga
 sagaMiddleware.run(helloSaga);

 // 省略ReactDOM.render部分的代碼...
複製代碼

這時候就能夠看到Hello Sagas!了;

代碼分析:

line 8 :經過redux-saga提供的工廠函數 createSagaMiddleware 建立 sagaMiddleware(固然建立時,你也能夠傳遞一些可選的配置參數)。

line 11-14 : 建立 store 實例, 並注入 saga中間件。意味着:以後每次執行 store.dispatch(action),數據流都會通過 sagaMiddleware 這一道工序,進行必要的 「加工處理」(好比:發送一個異步請求)。

line 17 : 啓動 saga,調用run方法使得generator能夠開始執行,也就是執行 rootSaga。一般是程序的一些初始化操做(好比:初始化數據、註冊 action 監聽)。

三、接下來加入異步調用的流程

先看下要實現的效果:

計數器

省略UI代碼;

reducer中已有加一的處理:

...
case 'INCREMENT':
  return {
    ...state,
    count: state.count + 1
}
...
複製代碼

sagas.js:

import { all, put, takeEvery } from 'redux-saga/effects'
 const delay = (ms) => new Promise(res => setTimeout(res, ms))

 // worker Saga: 執行異步的 increment 任務
 export function* incrementAsync() {
   yield delay(1000) // middleware 拿到一個 yield 後的 Promise,暫停1s後再繼續執行
   yield put({ type: 'INCREMENT' })  // 告訴 middleware 發起一個 INCREMENT 的 action。
 }

 // watcher Saga: 在每一個INCREMENT_ASYNC上生成一個新的incrementAsync任務
 export function* watchIncrementAsync() {
  yield takeEvery('INCREMENT_ASYNC', incrementAsync)
 }

// 啓動saga們
 export default function* rootSaga() {
  yield all([
    watchIncrementAsync(),
    helloSaga()
  ])
 }
複製代碼

把saga和store聯繫起來的代碼和上面類似,就是把helloSaga替換成rootSaga便可;

代碼分析:

Sagas 是被實現爲 Generator functions 的 line 2 : 建立一個delay函數,返回一個Promise,它在指定的毫秒數後解析。 line 5-8 : incrementAsync 這個 Saga 會暫停,直到 delay 返回的 Promise 被 resolve,即 1000ms 以後; line 6 : middleware 拿到一個 yield 後的 Promise,middleware 暫停 Saga,直到 Promise 完成。一旦 Promise 被 resolve,middleware 會恢復 Saga 接着執行,直到遇到下一個 yield。 line 7 : 這裏就是第二個yield啦,這裏的 put({type: 'INCREMENT'}) 就是一個Effect,Effect 是純js對象,其中包含了給 middleware 執行的指令;當 middleware 拿到被Saga yield的Effect的時候,也會暫停Saga,直到Effect 執行完成,而後Saga 會再次被恢復。 line 11-13 : 寫一個watcher saga,用redux-saga的api takeEvery 來監聽全部的 INCREMENT_ASYNC action,並在 action 被匹配時執行 incrementAsync 任務。 line 15-18 : 有了Saga,,現添加一個rootSaga來負責啓動全部Saga,用了all api,若是有其餘Saga都能一塊兒啓動。

line 7 返回的是一個Effect,console('Effect', put({ type: 'INCREMENT' }))

Effect

基於redux的數據流: 狀態決定展示,交互就是改狀態

redux的數據流

基於redux-saga的一次完整單向數據流:

完整單向數據流

api

在第一次使用dva的時候,用的最多的api就是putcall,有時還有用select

❀Effect 建立器(creators)

一、put(action)

建立一個Effect描述信息,指示 middleware 向Store dispatch一個action

至關於在 saga 中調用 store.dispatch(action)。

二、select(selector, ...args)

建立一個Effect,指示 middleware 調用提供的選擇器獲取 Store state 上的數據,即獲取狀態

三、call(fn, ...args)

建立一個Effect描述信息,指示 middleware 以args爲參數調用fn;

即執行fn(...args); 若是fn是個Generator,或者返回Promise,那麼會阻塞當前 saga 的執行,直到被調用函數 fn 返回結果,纔會執行下一步代碼。

四、take(pattern)

建立一個Effect描述信息,指示 middleware 等待 Store 上指定的 action。 Generator 會暫停(被阻塞了),直到一個與 pattern 匹配的 action 被髮起。 有種事件監聽的感受。 take的返回值是action

若是調用take而沒有參數或是'*',則全部調度的操做都匹配(例如,take()將匹配全部操做)

能夠監聽多個,eg: yield take(['LOGOUT', 'LOGIN_ERROR'])

五、fork(fn, ...args)

建立一個Effect描述信息,指示 middleware 以 無阻塞調用 方式執行 fn fork的返回值是task

相似於 call effect,區別在於它不會阻塞當前 saga,如同後臺運行通常,會當即返回一個 task 對象。 yield fork(fn ...args) 的結果是一個 Task 對象 —— 具備一些有用方法和屬性的對象。

六、cancel(task)

建立一個Effect描述信息,針對 fork 方法返回的 task ,能夠進行取消關閉。

七、cancelled()

建立一個Effect描述信息,指示 middleware 返回 該 generator 是否已經被取消。一般你會在 finally 區塊中使用這個 Effect 來運行取消時專用的代碼。

❀在強大的低階 API 之上構建的 wrapper effect

八、takeEvery(pattern, saga, ...args)

被 dispatch 的 action 中,在匹配到 pattern 的每個 action 上派生一個 saga takeEvery 是一個使用 take 和 fork 構建的高級 API。

實現:

const takeEvery = (patternOrChannel, saga, ...args) => fork(function*() {
  while (true) {
    const action = yield take(patternOrChannel)
    yield fork(saga, ...args.concat(action))
  }
})
複製代碼

九、takeLastest(pattern, saga, ...args)

被 dispatch 的 action 中,在匹配 pattern 的每個 action 上派生一個 saga。並自動取消以前全部已經啓動但仍在執行中的 saga 任務。 takeLatest 也是一個使用 take 和 fork 構建的高級 API。 實現:

const takeLatest = (patternOrChannel, saga, ...args) => fork(function*() {
  let lastTask
  while (true) {
    const action = yield take(patternOrChannel)
    if (lastTask) {
      yield cancel(lastTask) // 若是任務已經結束,cancel 則是空操做
    }
    lastTask = yield fork(saga, ...args.concat(action))
  }
})
複製代碼

❀Effect 組合器(combinators)

十、race([...effects])

建立一個Effect描述信息,指示 middleware 在多個 Effect 之間運行一個 race(與 Promise.race([...]) 的行爲相似)。

race能夠取到最快完成的那個結果,經常使用於請求超時

十一、all([...effects])

建立一個 Effect 描述信息,指示 middleware 並行運行多個 Effect,並等待它們所有完成。這是與標準的Promise#all相對應的 API。

也可用[...effects],yield 一個包含 effects 的數組,eg:

import { call } from 'redux-saga/effects'

// 正確寫法, effects 將會同步執行
const [users, repos] = yield [
  call(fetch, '/users'),
  call(fetch, '/repos')
];
複製代碼

generator 會被阻塞直到全部的 effects 都執行完畢,或者當一個 effect 被拒絕 (就像 Promise.all 的行爲)。

欲瞭解其餘api能夠訪問: 速查直達

返回

在dva中使用

dva中使用

對於try catch的額外補充

Call vs Fork

saga 中 call 和 fork 都是用來執行指定函數 fn,區別在於:

  • call Effect 會阻塞當前 saga 的執行,直到被調用函數 fn 返回結果才執行下一步代碼。
  • fork Effect 則不會阻塞當前 saga,會當即返回一個 task 對象。

fork 的異步非阻塞特性更適合於在後臺運行一些不影響主流程的代碼

高級概念

一、監聽將來的action —— take

先看下要實現的效果:

日誌記錄器

take的實現:

import { select, take } from 'redux-saga/effects'

 function* watchAndLog() {
  while (true) {
    const action = yield take('*');
    const state = yield select();
    console.log('action', action);
    console.log('state after', state);
  }
 }
複製代碼

代碼分析:

這是一個簡單的打印日誌功能 line 5: 指示 middleware 等待一個特定的 action。這裏整個Generator被暫停了,直到匹配到的action被dispatch了,這裏是*,因此是任意一個action; yield take('*')的返回值就是匹配到的action line 6: 用select api 拿到全部狀態 line 4-9: 這裏用了while(true),由於 Generator 函數不具有 從運行至完成 的行爲(run-to-completion behavior),這個Generator 會每次迭代到第5行時阻塞,以等待 action 發起。

對比takeEvery,實現同樣的效果:

import { select, takeEvery } from 'redux-saga/effects'

function* watchAndLog() {
  yield takeEvery('*', function* logger(action) { // 這裏action被被動注入回調了
    const state = yield select()

    console.log('action', action)
    console.log('state after', state)
  })
}
複製代碼

能夠看出,takeEvery 的實現中, 匹配到action就執行回調, action就被動的被 push 到任務處理函數的。 每次 action 被匹配時任務處理函數就會一遍又一遍地被調用。而且它們也沒法控制什麼時候中止監聽。 而 take 的實現中,Saga 是本身主動 pull action 的,就像是在執行一個普通函數同樣: action = getNextAction()。主動拿到action就能夠控制中止,流程上更靈活;

eg: 監聽用戶的操做,並在用戶初次建立完三條 Todo 信息時顯示祝賀信息

import { take, put } from 'redux-saga/effects'

function* watchFirstThreeTodosCreation() {
  for (let i = 0; i < 3; i++) {
    const action = yield take('TODO_CREATED')
  }
  yield put({type: 'SHOW_CONGRATULATION'})
}
複製代碼

action被匹配到3次以後,Generator 會被回收而且相應的監聽不會再發生

主動拉取 action 可讓咱們使用熟悉的同步風格來描述咱們的控制流 eg: 監聽得來,還有順序

function* loginFlow() {
  while (true) {
    yield take('LOGIN')
    // ... perform the login logic
    yield take('LOGOUT')
    // ... perform the logout logic
  }
}
複製代碼

返回

二、無阻塞調用 —— fork

登陸流程案例

就着上面說的登陸登出流程,先提早看一段代碼(有問題的):

import { take, call, put } from 'redux-saga/effects'
 import Api from '...'

 function* authorize(user, password) {
  try {
    const token = yield call(Api.authorize, user, password)
    yield put({type: 'LOGIN_SUCCESS', token})
    return token
  } catch(error) {
     yield put({type: 'LOGIN_ERROR', error})
  }
 }

 function* loginFlow() {
   while(true) {
    const {user, password} = yield take('LOGIN_REQUEST')
    const token = yield call(authorize, user, password)
    if(token) {
      yield call(Api.storeItem({token}))
      yield take('LOGOUT')
      yield call(Api.clearItem('token'))
    }
   }
 }
複製代碼

line 16: 當 LOGIN_REQUEST 的action被匹配時,拿到用戶名密碼就去調用 authorize 這個Generator (PS: call 不只能夠用來調用返回 Promise 的函數。咱們也能夠用它來調用其餘 Generator 函數。 ) line 4-12: 拿到用戶名密碼以後就去執行真正的請求,這時候 authorize 就被阻塞了,等待着拿token;拿到 token 就 dispatch 登陸成功,返回token;登陸失敗就 dispatch 登陸失敗 line 18-22: 登陸成功以後就緩存token,而且監聽登出的action,當匹配LOGOUT,則清楚token

上面的代碼流程很清晰,就像閱讀同步代碼同樣,天然順序肯定了執行步驟,不用專門理解控制流(若是用takeEvery就會須要去理解)

可是,上面的代碼有問題。 當用戶點登陸以後,authorize 被阻塞,請求還沒返回,token還沒拿到,就在此刻,用戶又點了登出,那麼...

有問題的登陸流程

上面代碼的問題是 call 是一個會阻塞的 Effect。即 Generator 在調用結束以前不能執行或處理任何其餘事情,而後,LOGOUT 與調用 authorize 是 併發的,致使出問題了

因此,須要本小節的主角登場 —— ☆ fork ☆

fork 一個 任務,任務會在後臺啓動,Generator不會被阻塞,調用者能夠繼續它本身的流程,而不用等待被 fork 的任務結束。

具體改進以下:

import { fork, call, take, put, cancel } from 'redux-saga/effects'
 import Api from '...'

 function* authorize(user, password) {
  try {
    const token = yield call(Api.authorize, user, password)
    yield put({type: 'LOGIN_SUCCESS', token})
    yield call(Api.storeItem, {token})
  } catch(error) {
     yield put({type: 'LOGIN_ERROR', error})
  } finally {
     if (yield cancelled()) {
       // 取消task以後的操做,好比取消loading之類
     }
   }
 }

 function* loginFlow() {
   while (true) {
     const {user, password} = yield take('LOGIN_REQUEST')
     const task = yield fork(authorize, user, password)
     const action = yield take(['LOGOUT', 'LOGIN_ERROR'])
     if (action.type === 'LOGOUT') {
       yield cancel(task)
     }
     yield call(Api.clearItem, 'token')
   }
 }
複製代碼

yield fork 的結果是一個Task Object. line 21: 改用 fork api 調用 authorize ,loginFlow 就不會被阻塞 line 22:監聽 2 個併發的 action, line 22-26: 會有三種狀況: 一、在登出以前,token已經拿到了,那麼會 dispatch LOGIN_SUCCESS,就結束了,就算在登出流程也是正常的 二、在登出以前,登陸失敗了,那麼會 dispatch LOGIN_ERROR ,而後清除token,結束;進入另一個 while 迭代等待下一個 LOGIN_REQUEST 三、token還沒拿到,用戶就登出了,那 loginFlow 會匹配到 LOGOUT ,取消掉 authorize 處理進程,清除token,而後就等待下一個 LOGIN_REQUEST 了 line 8: 使用 fork 以後就拿不到token了,由於不該該等待它,因此將 token 存儲操做移到 authorize 任務內部了 line 11-15:若是task被取消以後,你還須要作一些操做,好比Loading原本是true的,你想改爲false,那能夠利用canceled這個api來肯定是否取消了

三、在多個 Effects 之間啓動 race eg: 觸發一個遠程的獲取請求,而且限制了 1 秒內響應,不然做超時處理

import { race, call, put } from 'redux-saga/effects'
import { delay } from 'redux-saga'
function* fetchPostsWithTimeout() {
  const {posts, timeout} = yield race({
    posts: call(fetchApi, '/posts'),
    timeout: call(delay, 1000)
  })
  if (posts) {
    put({type: 'POSTS_RECEIVED', posts})
  } else {
    put({type: 'TIMEOUT_ERROR'})
  }
}
複製代碼

四、經過yield*進行排序

function* playLevelOne() { ... }
function* playLevelTwo() { ... }
function* playLevelThree() { ... }

function* game() {
  // 利用 yield* 組織saga的順序
  const score1 = yield* playLevelOne()  // ※
  yield put(showScore(score1))

  const score2 = yield* playLevelTwo()   // ※
  yield put(showScore(score2))

  const score3 = yield* playLevelThree()   // ※
  yield put(showScore(score3))
}
複製代碼

更多高級概念,可直達這裏學習

返回

對比redux-thunk

通常狀況下,action 都是符合 FSA 標準的(即:a plain javascript object),以下:

{
  type: 'ADD_TODO',
  payload: {
    text: 'Do something.'
  }
}
複製代碼

含義:當執行dispatch(action)時,通知reducer,而且把action.payload (新狀態數據)action.type的方式(操做) 同步更新 到本地store。

可是,涉及請求的時候,payload通常來自於遠程服務端;而後redux-thunk就以 middleware 的形式來加強 redux store 的 dispatch 方法,(即支持 dispatch(function)),看下面代碼:

// action.js
// -----------------
// 符合 FSA 的 action
export const setReplyModalData = (data) => {
    return { type: SET_REPLY_MODAL_DATA, payload:{data} };
};

// 這個 action return 的是一個function
// function 中包含了業務數據請求代碼邏輯
export function fetchData(someValue) {
  return (dispatch, getState) => {
    myAjaxLib.post("/someEndpoint", { data: someValue })
      .then(response => dispatch({ type: "REQUEST_SUCCEEDED", payload: response })
      .catch(error => dispatch({ type: "REQUEST_FAILED", error: error });
  };
}

// component.js
// ------------
// View 層 dispatch(fn) 觸發異步請求
// 這裏省略部分代碼
this.props.dispatch(fetchData({ hello: 'saga' }));

複製代碼

一樣的代碼,redux-saga的實現: 它單獨起一個新文件saga.js,而後把異步action遷移到裏面

// saga.js
// ----------
// worker saga
// 它是一個 generator function
// function 中也包含了業務數據請求代碼邏輯,但 是同步的寫法
function* fetchData(action) {
  const { payload: { someValue } } = action;
  try {
    const result = yield call(myAjaxLib.post, "/someEndpoint", { data: someValue });
    yield put({ type: "REQUEST_SUCCEEDED", payload: response });
  } catch (error) {
    yield put({ type: "REQUEST_FAILED", error: error });
  }
}

// watcher saga
// 監聽每一次 dispatch(action)
// 若是 action.type === 'REQUEST',那麼執行 fetchData
export function* watchFetchData() {
  yield takeEvery('REQUEST', fetchData);
}

// component.js
// -------
// View 層 dispatch(action) 觸發異步請求
// 這裏的 action 依然能夠是一個 plain object
this.props.dispatch({
  type: 'REQUEST',
  payload: {
    someValue: { hello: 'saga' }
  }
});

// action.js
// 而後action裏就保持了全部都是符合FSA的action了,更乾淨
export const setReplyModalData = (data) => {
    return { type: SET_REPLY_MODAL_DATA, payload:{data} };
};

複製代碼

綜上,redux-saga對比redux-thunk的優勢:

  • 反作用轉移到單獨的saga.js中,再也不摻雜在action.js中,保持 action 的簡單純粹,又使得異步操做集中能夠被集中處理。

  • dispatch 的參數依然是一個純粹的 action (FSA),而不是充滿 「黑魔法」 thunk function。

  • 每個 saga 都是 一個 generator function,代碼採用 同步書寫 的方式來處理 異步邏輯(No Callback Hell),代碼變得更易讀

  • 受益於 generator function 的 saga 實現,代碼異常/請求失敗 均可以直接經過 try/catch 語法直接捕獲處理。

返回

額外補充

請求都要加上try catch,多考慮,避免網頁掛掉; 那何時寫具體的catch呢?

感受 若是是要獲取數據的時候最好寫清楚catch,由於這種狀況下後端的toast通常都是網絡請求失敗這種mw,省得拿不到數據就啥也作不了了,這時,做爲前端,能夠給到用戶一個友好的toast。若是後端沒返回errmsg,頁面也沒任何提示就特別不友好若是是建立、編輯、新增等,就不須要前端去作toast,在接口統一處去toast後端返回的error message,才能夠toast具體的緣由,好比羣組重名了(這個是須要後端去查庫的)?仍是什麼數據不合法?仍是其餘緣由,就是提交類型的接口,在前端能作的表單校驗完成以後,仍是有接口報錯,那是後端才能檢查出來的,就toast後端的拋出來的問題;

最後總結一下

  • redux-saga就是一個redux的中間件,用於更優雅的管理異步
  • redux-saga有一堆的api可供使用
  • 能夠利用同步的方式處理異步邏輯,便於捕獲異常,易於測試;

參考連接

Redux-Saga Tutorial

Redux-Saga Tutorial中文版

redux-saga 漫談

相關文章
相關標籤/搜索