redux-saga實現與原理

介紹redux-saga使用和經常使用api介紹的文章不少,可是真正介紹原理的卻不多,下面我用本身的思路講一下redux-saga的執行過程。源碼有不少刪減,感興趣的可自行查看。html

1.react-saga是什麼

redux-saga是用於管理 Side Effects(異步操做/反作用)的redux中間件react

2.什麼是redux中間件

redux中間件是經過改變store.dispatch使dispatch(action)時能處理反作用。在這裏咱們用於處理異步操做。
具體可參考http://www.ruanyifeng.com/blo...。若是對中間件不熟悉,請必定把這篇文章看完再往下進行。ios

3.redux-saga特色

  • 集中處理redux反作用問題
  • 異步實現爲generator
  • watch/worker(監聽->執行)的工做形式

redux-saga主要是借鑑 sagas模式 和使用 generators 進行實現的。es6

首先,咱們講講sagas模式:解決長時間運行的事務致使的系統運行效率以及併發能力的問題,將業務分爲多個獨立的事務,每一個業務都會確保擁有修正事務(回滾),若是業務過程遇到了錯誤的狀況而且沒法繼續,它就能夠執行修正事務來修正已經完成的步驟,這樣以保證最終的一致性。舉個栗子:A事務須要等待B事務完成以後才能執行,若是B事務須要的時間很長,那麼A事務就要等好久才能執行,若是用sagas模式,能夠把A事務和B事務分別執行,若是B事務執行失敗,則把A事務進行回滾。編程

sagas模式參考文檔:https://blog.csdn.net/ethanwh...redux

redux-saga中對sagas的借鑑:在redux-saga裏,一個saga就是一個生成器函數(generator function),能夠在系統內無限期運行。當特定action被dispatch時,saga就能夠被喚醒。saga也能夠繼續dispatch額外的actions,也能夠接入程序的單一狀態樹。也能從主應用程序啓動,暫停和取消。借鑑了sagas的知足特殊條件的長事務,可回滾。axios

接下來說講generator
Generator 函數是 ES6 提供的一種異步編程解決方案。執行 Generator 函數會返回一個遍歷器對象,也就是說,Generator 函數除了狀態機,仍是一個遍歷器對象生成函數。返回的遍歷器對象,能夠依次遍歷 Generator 函數內部的每個狀態。形式上,Generator 函數是一個普通函數,可是有兩個特徵。一是,function關鍵字與函數名之間有一個星號;二是,函數體內部使用yield表達式,定義不一樣的內部狀態(yield在英語裏的意思就是「產出」)。爲了方便,下文中gen是generator的簡稱。後端

舉個簡單的例子api

function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}

var hw = helloWorldGenerator();

執行結果數組

hw.next()
// { value: 'hello', done: false }

hw.next()
// { value: 'world', done: false }

hw.next()
// { value: 'ending', done: true }

hw.next()
// { value: undefined, done: true }

再來一個複雜一點的傳值的例子

function* foo(x) {
  var y = 2 * (yield (x + 1));
  var z = yield (y / 3);
  return (x + y + z);
}
var b = foo(5);

執行結果

b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }
b.next(13) // { value:42, done:true }

具體可參考文檔http://es6.ruanyifeng.com/#do...,這裏很重要,請了解一下。

瞭解完上面的基礎,下面咱們開始講redux-saga的執行過程。

咱們以一個demo的形式進行分析。
須要解決幾個問題:

  • 怎麼執行監聽執行?
  • 如何循環把每一步執行完?
  • 怎麼處理下一次任務?

圖片描述

當咱們點擊按鈕時,會從後端請求接口,將返回的數據更新到頁面上

咱們先本身實現一箇中間件解決這個需求:

import axios from 'axios';

const takers = []; // 存generator

// 執行gen
function runGen(dispatch, gen) {
    // 防止無限循環
    if (!takers.length) {
        return;
    }
    // 遍歷執行generator
    takers.forEach((taker, key) => {
        const g = taker();
        // 刪除已執行的taker
        takers.splice(key, 1);
        // 執行yield axios.post並返回一個promise
        const userPromise = g.next().value;
        userPromise.then((user) => {
            // 把{dispatch, user}設置爲上一步的返回值,並執行yield dispatch
            g.next({ dispatch, user });
            // 執行yield takers
            g.next()  
        });
    })
}

export default function fetchMiddleware(_ref2) {
    var getState = _ref2.getState,
        dispatch = _ref2.dispatch;
    // 初始化時註冊taker,把generator添加到takers,用於dispatch時執行
    fetchMiddleware.run = () => takers.push(gen)
    // 改變dispatch
    return (next) => {
        return (action) => {
            // dispatch時執行這裏
            var result = next(action);
            runGen(dispatch, gen)
            return result;
        };
    };
    return fetchMiddleware;
}

怎麼用呢

import fetchMiddleware from './fetchMiddleware';
// 初始化
fetchMiddleware.run()
// 須要執行的gen
function* gen(){
    const { dispatch, user } = yield axios.post('http://rest.learncode.academy/api/wetern/users');
    const { data } = user;
    yield dispatch({ type: 'UPDATE_USER', payload: data })
    yield takers.push(gen)
}

如今咱們來看看這個中間件實現步驟

點擊按鈕,執行dispatch({ type: 'FEATCH_USER })

對於上面那個例子,咱們用gen.next()實現一步一步執行。

  1. 初始化時把gen添加到takers中,這樣作的目的是點擊按鈕的時候能夠執行generator
  2. 點擊按鈕的時候,獲取generator,而後從takers中刪除,防止dispatch({ type: 'UPDATE_USER', payload: data })也就是更新user的時候再次執行generator,形成循環調用
  3. gen.next()也就是執行yield axios.post('xxx'),這裏返回的是一個promise
  4. 在promise.then中調用gen.next({ dispatch, user }),其實是調用yield dispatch({ type: 'UPDATE_USER', payload: data })
  5. 最後調用gen.next,實際上調用yield takers.push(gen)。這裏是爲了把generator添加到takers中,用於下一次上面第二步的時候用。

這裏咱們簡單的實現了這個需求,redux-saga提供了更多更強大的api,下面咱們看看redux-saga是怎麼實現的吧。

先來看咱們代碼中如何使用redux-saga吧

./index.js
import { put, call, take } from 'redux-saga/effects';
import { createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';

// 建立action
const action = type => store.dispatch({ type })
// 建立redux-saga中間件
const sagaMiddleware = createSagaMiddleware()
// 生成store
const store = createStore(rootReducer, applyMiddleware(sagaMiddleware))
// 執行redux-saga中間件
sagaMiddleware.run(rootSaga)

function render() {
    ReactDOM.render(
        <App
            users={store.getState().users}
            onFetchUser={() => action('FETCH_USER')}
        />,
        document.getElementById('root'),
    )
}
./saga.js
// 建立saga
export function* rootSaga() {
    while(true) {
        yield take('FETCH_USER');
        const { data } =  yield call(axios.post, 'http://rest.learncode.academy/api/wetern/users');
        yield put({ type: 'UPDATE_USER', payload: data })
    }
}
  1. 首先引入redux-saga中間件,其實它就是redux的一箇中間件,經過改變dispatch(action),使咱們在發起action的時候處理異步操做。
function sagaMiddleware(_ref2) {
    var getState = _ref2.getState,
        dispatch = _ref2.dispatch;

    // next = store.dispatch 從redux中間件得出
    return function (next) {
      return function (action) {
        // dispatch(action) dispatch的時候會到這一步
        var result = next(action); // hit reducers
        // emitter 簡單的事件對象,subscribe訂閱/emit觸發
        sagaEmitter.emit(action);
        return result;
      };
    };
  }
// 發射器
export function emitter() {
  var subscribers = [];

  // 存儲須要訂閱的方法,並返回一個取消訂閱的方法
  function subscribe(sub) {
    subscribers.push(sub);
    return function () {
      return remove(subscribers, sub);
    };
  }

  // 這裏執行全部訂閱方法,咱們點擊頁面上的按鈕,執行dispatch的時候會執行訂閱器裏的函數
  function emit(item) {
    var arr = subscribers.slice();
    for (var i = 0, len = arr.length; i < len; i++) {
      arr[i](item);
    }
  }
}

dispatch的時候會執行訂閱器裏的函數,那麼訂閱器裏的函數是什麼呢,咱們接着看第二步

  1. 初始化的時候調用一次saga,這個只調用一次。調用saga的目的把獲取管道中taker的方法push到訂閱函數中,同時得到一個task。Task 接口指定了經過 fork,middleare.run 或 runSaga 運行 Saga 的結果。
方法 返回值
task.isRunning() 若任務還未返回或拋出了一個錯誤則爲 true
task.isCancelled() 若任務已被取消則爲 true
task.result() 任務的返回值。若任務仍在運行中則爲 undefined
task.error() 任務拋出的錯誤。若任務仍在執行中則爲 undefined
task.done 一個 Promise,如下兩者之一:1.以任務的返回值 2.resolve以任務拋出的錯誤 reject
task.cancel() 取消任務(若是任務仍在執行中)
export function runSaga(storeInterface, saga) {
  // iterator是封裝後的rootSaga
  var task = proc(iterator, subscribe, wrapSagaDispatch(dispatch), getState, context,
   { sagaMonitor: sagaMonitor, logger: logger, onError: onError }, effectId, saga.name);

  return task;
}

下面看看執行proc中發生了什麼
上面的iterator是對rootSaga這個generator函數的封裝,在proc裏redux-saga會執行gen.next,就會執行到咱們的yield take('FETCH_USER');而後會返回value={TAKE:{pattern: "FETCH_USER"}},根據返回的值,判斷會執行runTakeEffect()函數,在這個函數裏,會註冊一個taker,並把next添加到管道的taker中,到這裏就結束了調用,並返回一個task。

export default function proc(iterator) {
  // 執行這裏會把獲取管道中taker的方法(chan.put)push到subscribers,因此上面第一步執行訂閱中的方法其實是執行chan.put(input)
  var stdChannel = _stdChannel(subscribe);
  // 這裏的task在第一次執行的時候直接返回
  var task = newTask(parentEffectId, name, iterator, cont);

  next();

  function next(arg, isErr) {
      var result = void 0;
      // iterator是封裝後的rootSaga,這裏執行到的是yield take('FETCH_USER');
      // 返回value={TAKE:{pattern: "FETCH_USER"}}
      result = iterator.next(arg);
      // 根據返回的value,這裏會執行
      runEffect(result.value, parentEffectId, '', next);
  }
}
// 這裏cb是next
  function runTakeEffect(_ref2, cb) {
    var channel = _ref2.channel,
        pattern = _ref2.pattern,
        maybe = _ref2.maybe;

    channel = channel || stdChannel;
    var takeCb = function takeCb(inp) {
      return cb(inp);
    };
    // 給管道註冊taker,把next函數放到takers數組中
    channel.take(takeCb, matcher(pattern));
  }
  1. 點擊獲取用戶信息的按鈕(onFetchUser={() => action('FETCH_USER')}),由於咱們加入了saga中間件,讓咱們發起store.dispatch({ type: FETCH_USER })的時候會處理異步操做,

下面是saga中間件的主要代碼

// next = store.dispatch 從redux中間件得出
    return function (next) {
      return function (action) {
        // dispatch(action)
        var result = next(action); // hit reducers
        // 發射全部訂閱方法
        sagaEmitter.emit(action);
        return result;
      };
    };

下面咱們來看看sagaEmitter.emit(action)會作什麼

// 發射器
export function emitter() {
    ...
  // 發射全部訂閱方法
  function emit(item) {
    var arr = subscribers.slice();
    for (var i = 0, len = arr.length; i < len; i++) {
      arr[i](item);
    }
  }
}

這裏會遍歷訂閱函數,並執行訂閱函數裏的方法。在初始化的是咱們已經把獲取管道中taker的方法push到訂閱函數了,

因此咱們這裏執行的是獲取管道中的taker。

function put(input) {
      ...
    for (var i = 0; i < takers.length; i++) {
      var cb = takers[i];
      if (!cb[MATCH] || cb[MATCH](input)) {
        takers.splice(i, 1); // 刪除已經執行過的taker
        return cb(input); // 這裏cb其實是next
      }
    }
  }

在初始化執行yield take('FETCH_USER')的時候,已經把gen.next放入到takers中,這裏cb(input)其實是執行gen.next({ type: FETCH_USER }),
由於在初始化的時候gen函數已經執行了一次gen.next,如今執行gen.next則爲const { data } = yield call(axios.post, 'http://rest.learncode.academy...'),同時把{ type: FETCH_USER }做爲上一步的值傳入。執行yeild call返回value
{CALL:{args: [url]}},根據返回值,

這裏會執行源碼中的

function runCallEffect(_ref4, effectId, cb) {
    const result = fn.apply(context, args);
    // 這裏執行結果是promise
    return resolvePromise(result, cb);
  }
function resolvePromise(promise, cb) {
    // cb是gen.next,這裏把yield call的返回值傳遞給gen.next
    promise.then(cb, function (error) {
      return cb(error, true);
    });
  }

接下來gen.next執行到的是yield put({ type: 'UPDATE_USER', payload: data }),執行的返回值value
{PUT:{action:{payload:{id: "xx",type:"UPDATE_USER"}}}},根據返回值,

這裏會執行源碼中的

function runPutEffect(_ref3, cb) {
    var channel = _ref3.channel,
        action = _ref3.action;

    asap(function () {
      var result = (channel ? channel.put : dispatch)(action); // 實際上咱們演示的這段代碼,這裏會執行dispatch(action)

      return cb(result);
    });
  }

執行dispatch(action)這裏又會回到中間件中再次進入第三步的開始過程。並完成更新。
此次執行到遍歷takers的地方,takers已經爲空數組,會直接return,至此完成了整個獲取接口到更新數據。
因爲while(true)循環,再次執行yield take('FETCH_USER')。

下面附上兩張執行流程圖

  1. saga初始化

圖片描述

  1. dispatch

圖片描述

這裏只解釋了執行流程和幾個api,更多的請參考文檔https://redux-saga-in-chinese...

本文經過簡單實現了幾個effect方法來地介紹了redux-saga的原理,要真正作到redux-saga的全部功能,只須要再添加一些細節就能夠了

注:上面的源碼均爲刪減版,可自行查看源碼。我的文章,轉載請註明出處。

相關文章
相關標籤/搜索