聊一聊 redux 異步流之 redux-saga

React+Redux Cycle(來源:https://www.youtube.com/watch?v=1QI-UE3-0PU)

讓我驚訝的是,redux-saga 的做者居然是一名金融出身的在一家房地產公司工做的員工(讓我想到了阮老師。。。),可是他對寫代碼有着很是濃厚的熱忱,喜歡學習和挑戰新的事物,並探索新的想法。恩,牛逼的人不須要解釋。javascript

1. 介紹

對於歷來沒有據說過 redux-saga 的人,做者會如何描述它呢?html

It is a Redux middleware for handling side effects. —— Yassine Elouafijava

這裏包含了兩個信息:ios

首先,redux-saga 是一個 redux 的中間件,而中間件的做用是爲 redux 提供額外的功能。git

其次,咱們都知道,在 reducers 中的全部操做都是同步的而且是純粹的,即 reducer 都是純函數,純函數是指一個函數的返回結果只依賴於它的參數,而且在執行過程當中不會對外部產生反作用,即給它傳什麼,就吐出什麼。可是在實際的應用開發中,咱們但願作一些異步的(如Ajax請求)且不純粹的操做(如改變外部的狀態),這些在函數式編程範式中被稱爲「反作用」。github

Redux 的做者將這些反作用的處理經過提供中間件的方式讓開發者自行選擇進行實現。編程

redux-saga 就是用來處理上述反作用(異步任務)的一箇中間件。它是一個接收事件,並可能觸發新事件的過程管理者,爲你的應用管理複雜的流程。json

2. 先說一說 redux-thunk

redux-thunkredux-saga 是 redux 應用中最經常使用的兩種異步流處理方式。redux

From a synchronous perspective, a Thunk is a function that has everything already that it needs to do to give you some value back. You do not need to pass any arguments in, you simply call it and it will give you value back.
從異步的角度,Thunk 是指一切都就緒的會返回某些值的函數。你不用傳任何參數,你只需調用它,它便會返回相應的值。—— Rethinking Asynchronous Javascriptaxios

redux-thunk 的任務執行方式是從 UI 組件直接觸發任務。

舉個栗子:

假如當每次 Button 被點擊的時候,咱們想要從給定的 url 中獲取數據,採用 redux-thunk, 咱們會這樣寫:

// fetchUrl 返回一個 thunk
function fetchUrl(url) {
  return (dispatch) => {
    dispatch({
      type: 'FETCH_REQUEST'
    });

    fetch(url).then(data => dispatch({
      type: 'FETCH_SUCCESS',
      data
    }));
  }
}

// 若是 thunk 中間件正在運行的話,咱們能夠 dispatch 上述函數以下:
dispatch(
  fetchUrl(url)
):

redux-thunk 的主要思想是擴展 action,使得 action 從一個對象變成一個函數。

另外一個較完整的栗子:

// redux-thunk example
import {applyMiddleware, createStore} from 'redux';
import axios from 'axios';
import thunk from 'redux-thunk';

const initialState = { fetching: false, fetched: false, users: [], error: null }
const reducer = (state = initialState, action) => {
    switch(action.type) {
        case 'FETCH_USERS_START': {
            return {...state, fetching: true} 
            break;
        }
        case 'FETCH_USERS_ERROR': {
            return {...state, fetching: false, error: action.payload} 
            break;
        }
        case 'RECEIVE_USERS': {
            return {...state, fetching: false, fetched: true, users: action.payload} 
            break;
        }
    }
    return state;
}
const middleware = applyMiddleware(thunk);

// store.dispatch({type: 'FOO'});
// redux-thunk 的做用便是將 action 從一個對象變成一個函數
store.dispatch((dispatch) => {
    dispatch({type: 'FETCH_USERS_START'});
    // do something async
    axios.get('http://rest.learncode.academy/api/wstern/users')
        .then((response) => {
            dispatch({type: 'RECEIVE_USERS', payload: response.data})
        })
        .catch((err) => {
            dispatch({type: 'FECTH_USERS_ERROR', payload: err})
        })
});

redux-thunk 的缺點:
(1)action 雖然擴展了,但所以變得複雜,後期可維護性下降;
(2)thunks 內部測試邏輯比較困難,須要mock全部的觸發函數;
(3)協調併發任務比較困難,當本身的 action 調用了別人的 action,別人的 action 發生改動,則須要本身主動修改;
(4)業務邏輯會散佈在不一樣的地方:啓動的模塊,組件以及thunks內部。

3. redux-saga 是如何工做的?

sages 採用 Generator 函數來 yield Effects(包含指令的文本對象)。Generator 函數的做用是能夠暫停執行,再次執行的時候從上次暫停的地方繼續執行。Effect 是一個簡單的對象,該對象包含了一些給 middleware 解釋執行的信息。你能夠經過使用 effects APIforkcalltakeputcancel 等來建立 Effect。(redux-saga API 參考

yield call(fetch, '/products')yield 了下面的對象,call 建立了一條描述結果的信息,而後,redux-saga middleware 將確保執行這些指令並將指令的結果返回給 Generator:

// Effect -> 調用 fetch 函數並傳遞 `./products` 做爲參數
{
  type: CALL,
  function: fetch,
  args: ['./products']
}

redux-thunk 不一樣的是,在 redux-saga 中,UI 組件自身歷來不會觸發任務,它們老是會 dispatch 一個 action 來通知在 UI 中哪些地方發生了改變,而不須要對 action 進行修改。redux-saga 將異步任務進行了集中處理,且方便測試。

dispacth({ type: 'FETCH_REQUEST', url: /* ... */} );

全部的東西都必須被封裝在 sagas 中。sagas 包含3個部分,用於聯合執行任務:

  1. worker saga
    作全部的工做,如調用 API,進行異步請求,而且得到返回結果
  2. watcher saga
    監聽被 dispatch 的 actions,當接收到 action 或者知道其被觸發時,調用 worker saga 執行任務
  3. root saga
    當即啓動 sagas 的惟一入口
// example 1
import { take, fork, call, put } from 'redux-saga/effects';

// The worker: perform the requested task
function* fetchUrl(url) {
  const data = yield call(fetch, url);  // 指示中間件調用 fetch 異步任務
  yield put({ type: 'FETCH_SUCCESS', data });  // 指示中間件發起一個 action 到 Store
}

// The watcher: watch actions and coordinate worker tasks
function* watchFetchRequests() {
  while(true) {
    const action = yield take('FETCH_REQUEST');  // 指示中間件等待 Store 上指定的 action,即監聽 action
    yield fork(fetchUrl, action.url);  // 指示中間件以無阻塞調用方式執行 fetchUrl
  }
}

redux-saga 中的基本概念就是:sagas 自身不真正執行反作用(如函數 call),可是會構造一個須要執行做用的描述。中間件會執行該反作用並把結果返回給 generator 函數。

對上述例子的說明:

(1)引入的 redux-saga/effects 都是純函數,每一個函數構造一個特殊的對象,其中包含着中間件須要執行的指令,如:call(fetchUrl, url) 返回一個相似於 {type: CALL, function: fetchUrl, args: [url]} 的對象。

(2)在 watcher saga watchFetchRequests中:

首先 yield take('FETCH_REQUEST') 來告訴中間件咱們正在等待一個類型爲 FETCH_REQUEST 的 action,而後中間件會暫停執行 wacthFetchRequests generator 函數,直到 FETCH_REQUEST action 被 dispatch。一旦咱們得到了匹配的 action,中間件就會恢復執行 generator 函數。

下一條指令 fork(fetchUrl, action.url) 告訴中間件去無阻塞調用一個新的 fetchUrl 任務,action.url 做爲 fetchUrl 函數的參數傳遞。中間件會觸發 fetchUrl generator 而且不會阻塞 watchFetchRequests。當fetchUrl 開始執行的時候,watchFetchRequests 會繼續監聽其它的 watchFetchRequests actions。固然,JavaScript 是單線程的,redux-saga 讓事情看起來是同時進行的。

(3)在 worker saga fetchUrl 中,call(fetch,url) 指示中間件去調用 fetch 函數,同時,會阻塞fetchUrl 的執行,中間件會中止 generator 函數,直到 fetch 返回的 Promise 被 resolved(或 rejected),而後才恢復執行 generator 函數。

另外一個栗子

// example 2
import { takeEvery } from 'redux-saga';
import { call, put } from 'redux-saga/effects';
import axois from 'axois';

// 1. our worker saga
export function* createLessonAsync(action) {
    try {
        // effects(call, put): 
        // trigger off the code that we want to call that is asynchronous 
        // and also dispatched the result from that asynchrous code.
        const response = yield call(axois.post, 'http://jsonplaceholder.typicode.com/posts', {section_id: action.sectionId});
        yield put({type: 'lunchbox/lessons/CREATE_SUCCEEDED', response: response.data});
    } catch(e) {
        console.log(e);
    }
}

// 2. our watcher saga: spawn a new task on each ACTION
export function* watchCreateLesson() {
    // takeEvery: 
    // listen for certain actions that are going to be dispatched and take them and run through our worker saga.
    yield takeEvery('lunchbox/lessons/CREATE', createLessonAsync);
}


// 3. our root saga: single entry point to start our sagas at once
export default function* rootSaga() {
    // combine all of our sagas that we create
    // and we want to provide all our Watchers sagas
    yield watchCreateLesson()
}

最後,總結一下 redux-saga 的優勢:

(1)聲明式 Effects:全部的操做以JavaScript對象的方式被 yield,並被 middleware 執行。使得在 saga 內部測試變得更加容易,能夠經過簡單地遍歷 Generator 並在 yield 後的成功值上面作一個 deepEqual 測試。
(2)高級的異步控制流以及併發管理:可使用簡單的同步方式描述異步流,並經過 fork 實現併發任務。
(3)架構上的優點:將全部的異步流程控制都移入到了 sagas,UI 組件不用執行業務邏輯,只需 dispatch action 就行,加強組件複用性。

4. 附上測試 demo

redux-async-demo

5. 參考

redux-saga - Saga Middleware for Redux to Handle Side Effects - Interview with Yassine Elouafi
redux-saga 基本概念
Redux: Thunk vs. Saga
從redux-thunk到redux-saga實踐
React項目小結系列:項目中redux異步流的選擇
API calls from Redux 系列

相關文章
相關標籤/搜索