淺析redux-saga實現原理

做者簡介 joey 螞蟻金服·數據體驗技術團隊html

項目中一直使用redux-saga來處理異步action的流程。對於effect的實現原理感到很好奇。抽空去研究了一下他的實現。本文不會描述redux-saga的基礎API和優勢,單純聊實現原理,歡迎你們在評論區留言討論。git

前言

redux-saga監聽action的代碼以下:github

import { takeEvery } from 'redux-saga';

function* mainSaga() {
  yield takeEvery('action_name', function* (action) {
    console.log(action);
  });
}
複製代碼

用generator到底是怎麼實現takeEvery的呢?咱們先來看稍微簡單一點的take的實現原理:redux

take實現原理

咱們嘗試寫一個demo,用saga的方式實現用generator監聽action。bash

$btn.addEventListener('click', () => {
  const action =`action data${i++}`;
  // trigger action
}, false);

function* mainSaga() {
  const action = yield take();
  console.log(action);
}
複製代碼

要在$btn點擊時候,可以讀到action的值。dom

channel

這裏咱們須要引入一個概念——channel異步

channel是對事件源的抽象,做用是先註冊一個take方法,當put觸發時,執行一次take方法,而後銷燬他。學習

channel的簡單實現以下:ui

function channel() {
  let taker;

  function take(cb) {
    taker = cb;
  }

  function put(input) {
    if (taker) {
      const tempTaker = taker;
      taker = null;
      tempTaker(input);
    }
  }

  return {
    put,
    take,
  };
}

const chan = channel();
複製代碼

咱們利用channel作generator和dom事件的鏈接,將dom事件改寫以下:spa

$btn.addEventListener('click', () => {
  const action =`action data${i++}`;
  chan.put(action);
}, false);
複製代碼

當put觸發時,若是channel裏已經有註冊了的taker,taker就會執行。

咱們須要在put觸發以前,先調用channel的take方法,註冊實際要運行的方法。

咱們繼續看mainSaga裏的實現。

function* mainSaga() {
  const action = yield take();
  console.log(action);
}
複製代碼

這個take是saga裏的一種effect類型。

先看effecttake()的實現。

function take() {
  return {
    type: 'take'
  };
}

複製代碼

出乎意料,僅僅返回了一個帶類型的object。

其實redux-saga裏全部effect返回的值,都是一個帶類型的純object對象。

那到底是何時觸發channel的take方法的呢?還須要從調用mainSaga的代碼上找緣由。

generator的特色是執行到某一步時,能夠把控制權交給外部代碼,由外部代碼拿到返回結果後,決定該怎麼作。

task

這裏咱們又要引入一個新的概念task

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

task的簡易實現以下:

function task(iterator) {
  const iter = iterator();
  function next(args) {
    const result = iter.next(args);
    if (!result.done) {
      const effect = result.value;
      if (effect.type === 'take) { runTakeEffect(result.value, next); } } } next(); } task(mainSaga); 複製代碼

yield take()運行時,將take()返回的結果交給外層的task,此時代碼的控制權就已經從gennerator方法中轉到了task裏了。

result.value的值就是take()返回的結果{ type: 'take' }

再看runTakeEffect的實現:

function runTakeEffect(effect, cb) {
  chan.take(input => {
    cb(input);
  });
}
複製代碼

到這裏,咱們終於看到調用channel的take方法的地方了。

完整代碼以下:

function channel() {
  let taker;

  function take(cb) {
    taker = cb;
  }

  function put(input) {
    if (taker) {
      const tempTaker = taker;
      taker = null;
      tempTaker(input);
    }
  }

  return {
    put,
    take,
  };
}

const chan = channel();

function take() {
  return {
    type: 'take'
  };
}

function* mainSaga() {
  const action = yield take();
  console.log(action);
}

function runTakeEffect(effect, cb) {
  chan.take(input => {
    cb(input);
  });
}

function task(iterator) {
  const iter = iterator();
  function next(args) {
    const result = iter.next(args);
    if (!result.done) {
      const effect = result.value;
      if (effect.type === 'take') {
        runTakeEffect(result.value, next);
      }
    }
  }
  next();
}

task(mainSaga);

let i = 0;
$btn.addEventListener('click', () => {
  const action =`action data${i++}`;
  chan.put(action);
}, false);
複製代碼

總體流程就是,先經過mainSaga往channel裏註冊了一個taker,一旦dom點擊發生,就觸發channel的put,put會消耗掉已經註冊的taker,這樣就完成了一次點擊事件的監聽過程。

查看在線demo

takeEvery實現原理

在上一節中,咱們已經模仿saga實現了一次事件監聽,可是仍是有問題,咱們只能監聽一次點擊,怎麼能作到監聽每次點擊事件呢?redux-saga提供了一個helper方法——takeEvery。咱們嘗試在咱們的簡易版saga中實現一下takeEvery

function* takeEvery(worker) {
  yield fork(function* () {
    while(true) {
      const action = yield take();
      worker(action);
    }
  });
}

function* mainSaga() {
  yield takeEvery(action => {
    $result.innerHTML = action;
  });
}
複製代碼

這裏用到了一個新的effect方法fork

fork

fork的做用是啓動一個新的task,不阻塞原task執行。代碼修改以下:

function fork(cb) {
  return {
    type: 'fork',
    fn: cb,
  };
}

function runForkEffect(effect, cb) {
  task(effect.fn || effect);
  cb();
}

function task(iterator) {
  const iter = typeof iterator === 'function' ? iterator() : iterator;
  function next(args) {
    const result = iter.next(args);
    if (!result.done) {
      const effect = result.value;

      // 判斷effect是不是iterator
      if (typeof effect[Symbol.iterator] === 'function') {
        runForkEffect(effect, next);
      } else if (effect.type) {
        switch (effect.type) {
        case 'take':
          runTakeEffect(effect, next);
          break;
        case 'fork':
          runForkEffect(effect, next);
          break;
        default:
        }
      }
    }
  }
  next();
}
複製代碼

咱們經過添加了一種新的effectfork,啓動了一個新的task takeEvery。

takeEvery的做用就是當channel的put發生後,自動往channel裏放進一個新的taker。

咱們實現的channel裏同時只能有一個taker,while(true)的做用就是每當一個put觸發消耗掉了taker後,就自動觸發runTakeEffect中傳入的task的next方法,再次往channel裏放進一個taker,從而作到源源不斷地監聽事件。

在線demo

effect的本質

經過上文的實現,咱們發現全部的yield後返回的effect,都是一個純object,用來給generator外層的執行容器task發送一個信號,告訴task該作什麼。

基於這種思路,若是咱們要新增一個effect,來cancel task,也能夠很容易實現。

首先咱們先定義一個cancel方法,用來發送cancel的信號。

function cancel() {
  return {
    type: 'cancel'
  };
}
複製代碼

而後修改task的代碼,讓他能真正執行cancel的邏輯。

function task(iterator) {
  const iter = typeof iterator === 'function' ? iterator() : iterator;
  ...

  function runCancelEffect() {
    // do some cancel logic
  }

  function next(args) {
    const result = iter.next(args);
    if (!result.done) {
      const effect = result.value;

      if (typeof effect[Symbol.iterator] === 'function') {
        runForkEffect(effect, next);
      } else if (effect.type) {
        switch (effect.type) {
        case 'cancel':
          runCancelEffect();
        case 'take':
          runTakeEffect(result.value, next);
          break;
        case 'fork':
          runForkEffect(result.value, next);
          break;
        default:
        }
      }
    }
  }
  next();
}
複製代碼

小結

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

對generator使用有興趣的同窗推薦學習一下redux-saga源碼。在此推薦一篇使用generator實現dom事件監聽的文章 繼續探索JS中的Iterator,兼談與Observable的對比

感興趣的同窗能夠關注專欄或者發送簡歷至'chaofeng.lcf####alibaba-inc.com'.replace('####', '@'),歡迎有志之士加入~

原文地址:github.com/ProtoTeam/b…

相關文章
相關標籤/搜索