redux-saga 原理淺析

原文地址git

前言

筆者最近在作一些後臺項目,使用的是Ant Design Pro,其使用了redux-saga處理異步數據流,本文將對redux-saga的原理作一個簡單的解讀,並將實現一個簡易版的redux-sagagithub

Generator函數的自動流程控制

在redux-saga中,saga是指一些長時操做,用generator函數表示。generator函數的強大之處在於其能夠手動的暫停、恢復執行,且能夠與函數體外進行數據交互,看以下例子:json

function *gen() {
  const a = yield 'hello';
  console.log(a);
}

cont g = gen();
g.next(); // { value: 'hello', done: false }
setTimeout(() => g.next('hi'), 1000)  // 此時 a => 'hi' 一秒後打印‘hi'
複製代碼

能夠看出來genrator函數什麼時候進行下一步操做徹底取決於外部的調度時機,且其內部執行狀態也由外部的輸入決定,這使得generator函數能夠很方便的作異步流程控制。舉個例子,咱們首先讀取一個文件的內容做爲查詢參數,而後請求一個查詢接口並把返回的內容打印出來:redux

function getParams(file) {
  return new Promise(resolve => {
    fs.readFile(file, (err, data) => {
      resolve(data)
    })
  })
}

function getContent(params) {
  // request返回promise
  return request(params)
}

function *gen() {
  const params = yield getParams('config.json');
  const content = yield getContent(params);
  console.log(content);
}
複製代碼

咱們能夠手動控制gen函數的執行:promise

const g = gen();
g.next().value.then(params => {
  g.next(params).value.then(content => {
    g.next(content);
  })
})
複製代碼

以上能夠達到咱們的目的,可是過於繁瑣,咱們想要的是generator函數能夠自動的執行,能夠寫一個簡易的自動執行函數以下:bash

function genRun(gen) {
  const g = gen();
  
  next();

  function next(err, pre) {
    let temp;
    (err === null) && (temp = g.next(pre));
    (err !== null) && (temp = g.throw(pre));

    if(!temp.done) {
      nextWithYieldType(temp.value, next);
    }
  }
}

function nextWithYieldType(value, next) {
  if(isPromise(value)) {
    value
      .then(success => next(null, success))
      .catch(error => next(error))
  } 
}

genRun(gen);
複製代碼

此時generator函數即可以自動執行,事實上咱們能夠發現,generator的內部狀態徹底是由nextWithYieldType決定的,咱們能夠根據yield的類型執行不一樣的處理邏輯。異步

Effect

事實上sagaMiddleware.run(saga)能夠相似看作genRun(saga),而saga是由一個個的effect組成的,那麼effect是什麼?redux-saga官網的解釋:一個 effect 就是一個 Plain Object JavaScript 對象,包含一些將被 saga middleware 執行的指令。redux-saga提供了不少Effect建立器,如callputtake等,已call爲例:函數

function saga*() {
  const result = yield call(genPromise);
  console.log(result);
}
複製代碼

call(genPromise)生成的就是一個effect,它可能相似以下:ui

{
  isEffect: true,
  type: 'CALL',
  fn: genPromise
}
複製代碼

事實上effect只代表了意圖,而實際的行爲由相似於上文的nextWithYieldType完成,例如:spa

function nextWithYieldType(value, next) {
  ...
  if(isCallEffect(value)) {
    value.fn(). then(success => next(null, success)).catch(error => next(error))  
  } 
}
複製代碼

當genPromise函數返回的promise被resolve後便會打印出結果。

生產者與消費者

觀察下面的例子

function *saga() {
  yield take('TEST');
  console.log('test...');
}

sagaMiddleware.run(test);
複製代碼

saga會在take('TEST')處阻塞,只有執行了dispatch({type: 'TEST'})後saga才能繼續運行(注意:此時的dispatch方法是通過sagaMiddleware包裝過的)。這給咱們的感受彷佛很像是take是一個生產者,在等待disaptch的消費,事實上take只是一個Effect生成器,具體的處理邏輯依然是在nextWithYieldType完成的,相似於:

function nextWithYieldType(value, next) {
  ...
  // take('TEST')生成的effect簡單的認爲是 {isEffect: true, type: 'TAKE', name: 'TEST'}
  if(isTakeEffect(value)) {
    channel.take({pattern: value.name, cb: params => next(null, params)})  
  } 
}
複製代碼

channel是一個任務生成器,它有兩個方法:take生成任務,put消費任務:

function channel() {
  /* task = { pattern, cb } */
  let _task = null;

  function take(task) {
    _task = task;
  }

  function put(pattern, args) {
    if(!_task) return;
    if(pattern == _task.pattern) _task.cb.call(null, args);
  }

  return {
    take,
    put
  }
}
複製代碼

顯然任務是在執行dispatch的時候被消費掉的,這個工做是在sagaMiddleware中作的,相似於以下:

const sagaMiddleware = store => {
  return next => action => {
    next(action);
    
    const { type, ...payload } = action;
    channel.put(type, payload);
  }
} 
複製代碼

看到這裏咱們能夠發現,須要咱們作的就是不斷的完善nextWithYieldType這個函數,當完成了putforktakeEvery對應的邏輯後,一個具有基本功能的redux-saga就誕生啦,筆者就不在贅述這些功能的實現了。最後,你能夠查看這裏:tiny-redux-saga,這是筆者實現的一個簡易版的redux-saga,但願對你有所幫助。


全文完。

相關文章
相關標籤/搜索