redux 異步流之 redux-saga


1. 介紹

對於從來沒有聽說過 redux-saga 的人,作者會如何描述它呢?

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

這裏包含了兩個信息:

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

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

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

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

2. 先說一說 redux-thunk

redux-thunk 和 redux-saga 是 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 Javascript

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 API 如 forkcalltakeputcancel 等來創建 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 的唯一入口
☀ 如何使用?

首先,我們得在文件入口中加入 saga 中間件,並且啓動它,它會一直運行:

//...
import { createStore, applyMiddleware} from 'redux';
import createSagaMiddleware from 'redux-saga';
import appReducer from './reducers';
//...

const sagaMiddleware = createSagaMiddleware();
const middlewares = [sagaMiddleware];

const store = createStore(appReducer, applyMiddleware(...middlewares));
sagaMiddleware.run(rootSaga);

render(
    <Provider store={store}>
        <App />
    </Provider>,
    document.getElementById('app')
);

然後,就可以在 sagas 文件夾中集中寫 saga 文件了:

// 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 axios from 'axios';

// 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(axios.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 系列


作者:Ruth92
鏈接:https://www.jianshu.com/p/e84493c7af35
來源:簡書
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章