天天看點

手寫Redux-Saga源碼

上一篇文章我們分析了

Redux-Thunk

的源碼,可以看到他的代碼非常簡單,隻是讓

dispatch

可以處理函數類型的

action

,其作者也承認對于複雜場景,

Redux-Thunk

并不适用,還推薦了

Redux-Saga

來處理複雜副作用。本文要講的就是

Redux-Saga

,這個也是我在實際工作中使用最多的

Redux

異步解決方案。

Redux-Saga

Redux-Thunk

複雜得多,而且他整個異步流程都使用

Generator

來處理,

Generator

也是我們這篇文章的前置知識,如果你對

Generator

還不熟悉,可以看看這篇文章。

本文仍然是老套路,先來一個

Redux-Saga

的簡單例子,然後我們自己寫一個

Redux-Saga

來替代他,也就是源碼分析。

本文可運作的代碼已經上傳到GitHub,可以拿下來玩玩:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/React/redux-saga

簡單例子

網絡請求是我們經常需要處理的異步操作,假設我們現在的一個簡單需求就是點選一個按鈕去請求使用者的資訊,大概長這樣:

手寫Redux-Saga源碼

這個需求使用

Redux

實作起來也很簡單,點選按鈕的時候

dispatch

出一個

action

。這個

action

會觸發一個請求,請求傳回的資料拿來顯示在頁面上就行:

import React from 'react';
import { connect } from 'react-redux';

function App(props) {
  const { dispatch, userInfo } = props;

  const getUserInfo = () => {
    dispatch({ type: 'FETCH_USER_INFO' })
  }

  return (
    <div className="App">
      <button onClick={getUserInfo}>Get User Info</button>
      <br></br>
      {userInfo && JSON.stringify(userInfo)}
    </div>
  );
}

const matStateToProps = (state) => ({
  userInfo: state.userInfo
})

export default connect(matStateToProps)(App);
           

上面這種寫法都是我們之前講

Redux

就介紹過的,

Redux-Saga

介入的地方是

dispatch({ type: 'FETCH_USER_INFO' })

之後。按照

Redux

一般的流程,

FETCH_USER_INFO

被發出後應該進入

reducer

處理,但是

reducer

都是同步代碼,并不适合發起網絡請求,是以我們可以使用

Redux-Saga

來捕獲

FETCH_USER_INFO

并處理。

Redux-Saga

是一個

Redux

中間件,是以我們在

createStore

的時候将它引入就行:

// store.js

import { createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
import reducer from './reducer';
import rootSaga from './saga';

const sagaMiddleware = createSagaMiddleware()

let store = createStore(reducer, applyMiddleware(sagaMiddleware));

// 注意這裡,sagaMiddleware作為中間件放入Redux後
// 還需要手動啟動他來運作rootSaga
sagaMiddleware.run(rootSaga);

export default store;
           

注意上面代碼裡的這一行:

sagaMiddleware.run(rootSaga);
           

sagaMiddleware.run

是用來手動啟動

rootSaga

的,我們來看看

rootSaga

是怎麼寫的:

import { call, put, takeLatest } from 'redux-saga/effects';
import { fetchUserInfoAPI } from './api';

function* fetchUserInfo() {
  try {
    const user = yield call(fetchUserInfoAPI);
    yield put({ type: "FETCH_USER_SUCCEEDED", payload: user });
  } catch (e) {
    yield put({ type: "FETCH_USER_FAILED", payload: e.message });
  }
}

function* rootSaga() {
  yield takeEvery("FETCH_USER_INFO", fetchUserInfo);
}

export default rootSaga;
           

上面的代碼我們從

export

開始看吧,

export

的東西是

rootSaga

這個

Generator

函數,這裡面就一行:

yield takeEvery("FETCH_USER_INFO", fetchUserInfo);
           

這一行代碼用到了

Redux-Saga

的一個

effect

,也就是

takeEvery

,他的作用是監聽每個

FETCH_USER_INFO

,當

FETCH_USER_INFO

出現的時候,就調用

fetchUserInfo

函數,注意這裡是每個

FETCH_USER_INFO

。也就是說如果同時發出多個

FETCH_USER_INFO

,我們每個都會響應并發起請求。類似的還有

takeLatest

takeLatest

從名字都可以看出來,是響應最後一個請求,具體使用哪一個,要看具體的需求。

然後看看

fetchUserInfo

函數,這個函數也不複雜,就是調用一個

API

函數

fetchUserInfoAPI

去擷取資料,注意我們這裡函數調用并不是直接的

fetchUserInfoAPI()

,而是使用了

Redux-Saga

call

effect

,這樣做可以讓我們寫單元測試變得更簡單,為什麼會這樣,我們後面講源碼的時候再來仔細看看。擷取資料後,我們調用了

put

去發出

FETCH_USER_SUCCEEDED

action

,這裡的

put

類似于

Redux

裡面的

dispatch

,也是用來發出

action

的。這樣我們的

reducer

就可以拿到

FETCH_USER_SUCCEEDED

進行處理了,跟以前的

reducer

并沒有太大差別。

// reducer.js

const initState = {
  userInfo: null,
  error: ''
};

function reducer(state = initState, action) {
  switch (action.type) {
    case 'FETCH_USER_SUCCEEDED':
      return { ...state, userInfo: action.payload };
    case 'FETCH_USER_FAILED':
      return { ...state, error: action.payload };
    default:
      return state;
  }
}

export default reducer;
           

通過這個例子的代碼結構我們可以看出:

  1. action

    被分為了兩種,一種是觸發異步處理的,一種是普通的同步

    action

  2. 異步

    action

    使用

    Redux-Saga

    來監聽,監聽的時候可以使用

    takeLatest

    或者

    takeEvery

    來處理并發的請求。
  3. 具體的

    saga

    實作可以使用

    Redux-Saga

    提供的方法,比如

    call

    put

    之類的,可以讓單元測試更好寫。
  4. 一個

    action

    可以被

    Redux-Saga

    Reducer

    同時響應,比如上面的

    FETCH_USER_INFO

    發出後我還想讓頁面轉個圈,可以直接在

    reducer

    裡面加一個就行:
    ...
    case 'FETCH_USER_INFO':
          return { ...state, isLoading: true };
    ...
               

手寫源碼

通過上面這個例子,我們可以看出,

Redux-Saga

的運作是通過這一行代碼來實作的:

sagaMiddleware.run(rootSaga);
           

整個

Redux-Saga

的運作和原本的

Redux

并不沖突,

Redux

甚至都不知道他的存在,他們之間耦合很小,隻在需要的時候通過

put

發出

action

來進行通訊。是以我猜測,他應該是自己實作了一套完全獨立的異步任務處理機制,下面我們從能感覺到的

API

入手,一步一步來探尋下他源碼的奧秘吧。本文全部代碼參照官方源碼寫成,函數名字和變量名字盡量保持一緻,寫到具體的方法的時候我也會貼出對應的代碼位址,主要代碼都在這裡:https://github.com/redux-saga/redux-saga/tree/master/packages/core/src

先來看看我們用到了哪些

API

,這些API就是我們今天手寫的目标:

  1. createSagaMiddleware:這個方法會傳回一個中間件執行個體

    sagaMiddleware

  2. sagaMiddleware.run: 這個方法是真正運作我們寫的

    saga

    的入口
  3. takeEvery:這個方法是用來控制并發流程的
  4. call:用來調用其他方法
  5. put:發出

    action

    ,用來和

    Redux

    通訊

從中間件入手

之前我們講

Redux

源碼的時候詳細分析了

Redux

中間件的原理和範式,一個中間件大概就長這個樣子:

function logger(store) {
  return function(next) {
    return function(action) {
      console.group(action.type);
      console.info('dispatching', action);
      let result = next(action);
      console.log('next state', store.getState());
      console.groupEnd();
      return result
    }
  }
}
           

這其實就相當于一個

Redux

中間件的範式了:

  1. 一個中間件接收

    store

    作為參數,會傳回一個函數
  2. 傳回的這個函數接收老的

    dispatch

    函數作為參數(也就是上面的

    next

    ),會傳回一個新的函數
  3. 傳回的新函數就是新的

    dispatch

    函數,這個函數裡面可以拿到外面兩層傳進來的

    store

    和老

    dispatch

依照這個範式以及前面對

createSagaMiddleware

的使用,我們可以先寫出這個函數的骨架:

// sagaMiddlewareFactory其實就是我們外面使用的createSagaMiddleware
function sagaMiddlewareFactory() {
  // 傳回的是一個Redux中間件
  // 需要符合他的範式
  const sagaMiddleware = function (store) {
    return function (next) {
      return function (action) {
        // 内容先寫個空的
        let result = next(action);
        return result;
      }
    }
  }
  
  // sagaMiddleware上還有個run方法
  // 是用來啟動saga的
  // 我們先留白吧
  sagaMiddleware.run = () => { }

  return sagaMiddleware;
}

export default sagaMiddlewareFactory;
           

梳理架構

現在我們有了一個空的骨架,接下來該幹啥呢?前面我們說過了,

Redux-Saga

很可能是自己實作了一套完全獨立的異步事件處理機制。這種異步事件處理機制需要一個進行中心來存儲事件和處理函數,還需要一個方法來觸發隊列中的事件的執行,再回看前面的使用的API,我們發現了兩個類似功能的API:

  1. takeEvery(action, callback):他接收的參數就是

    action

    callback

    ,而且我們在根

    saga

    裡面可能會多次調用它來注冊不同

    action

    的處理函數,這其實就相當于往進行中心裡面塞入事件了。
  2. put(action):

    put

    的參數是

    action

    ,他唯一的作用就是觸發對應事件的回調運作。

可以看到

Redux-Saga

這種機制也是用

takeEvery

先注冊回調,然後使用

put

發出消息來觸發回調執行,這其實跟我們其他文章多次提到的釋出訂閱模式很像。

手寫channel

channel

Redux-Saga

儲存回調和觸發回調的地方,類似于釋出訂閱模式,我們先來寫個:

export function multicastChannel() {
  const currentTakers = [];     // 一個變量存儲我們所有注冊的事件和回調

  // 儲存事件和回調的函數
  // Redux-Saga裡面take接收回調cb和比對方法matcher兩個參數
  // 事實上take到的事件名稱也被封裝到了matcher裡面
  function take(cb, matcher) {
    cb['MATCH'] = matcher;
    currentTakers.push(cb);
  }

  function put(input) {
    const takers = currentTakers;

    for (let i = 0, len = takers.length; i < len; i++) {
      const taker = takers[i]

      // 這裡的'MATCH'是上面take塞進來的比對方法
      // 如果比對上了就将回調拿出來執行
      if (taker['MATCH'](input)) {
        taker(input);
      }
    }
  }
  
  return {
    take,
    put
  }
}
           

上述代碼中有一個奇怪的點,就是将

matcher

作為屬性放到了回調函數上,這麼做的原因我想是為了讓外部可以自定義比對方法,而不是簡單的事件名稱比對,事實上

Redux-Saga

本身就支援好幾種比對模式,包括

字元串,Symbol,數組

等等。

内置支援的比對方法可以看這裡:https://github.com/redux-saga/redux-saga/blob/master/packages/core/src/internal/matcher.js。

channel

對應的源碼可以看這裡:https://github.com/redux-saga/redux-saga/blob/master/packages/core/src/internal/channel.js#L153

有了

channel

之後,我們的中間件裡面其實隻要再幹一件事情就行了,就是調用

channel.put

将接收的

action

再發給

channel

去執行回調就行,是以我們加一行代碼:

// ... 省略前面代碼

const result = next(action);

channel.put(action);     // 将收到的action也發給Redux-Saga

return result;

// ... 省略後面代碼
           

sagaMiddleware.run

前面的

put

是發出事件,執行回調,可是我們的回調還沒注冊呢,那注冊回調應該在什麼地方呢?看起來隻有一個地方了,那就是

sagaMiddleware.run

。簡單來說,

sagaMiddleware.run

接收一個

Generator

作為參數,然後執行這個

Generator

,當遇到

take

的時候就将它注冊到

channel

上面去。這裡我們先實作

take

takeEvery

是在這個基礎上實作的。

Redux-Saga

中這塊代碼是單獨抽取了一個檔案,我們仿照這種做法吧。

首先需要在中間件裡面将

Redux

getState

dispatch

等參數傳遞進去,

Redux-Saga

使用的是

bind

函數,是以中間件方法改造如下:

function sagaMiddleware({ getState, dispatch }) {
  // 将getState, dispatch通過bind傳給runSaga
  boundRunSaga = runSaga.bind(null, {
    channel,
    dispatch,
    getState,
  })

  return function (next) {
    return function (action) {
      const result = next(action);

      channel.put(action);

      return result;
    }
  }
}
           

然後

sagaMiddleware.run

就直接将

boundRunSaga

拿來運作就行了:

sagaMiddleware.run = (...args) => {
  boundRunSaga(...args)
}
           

注意這裡的

...args

,這個其實就是我們傳進去的

rootSaga

。到這裡其實中間件部分就已經完成了,後面的代碼就是具體的執行過程了。

中間件對應的源碼可以看這裡:https://github.com/redux-saga/redux-saga/blob/master/packages/core/src/internal/middleware.js

runSaga

runSaga

其實才是真正的

sagaMiddleware.run

,通過前面的分析,我們已經知道他的作用是接收

Generator

并執行,如果遇到

take

就将它注冊到

channel

上去,如果遇到

put

就将對應的回調拿出來執行,但是

Redux-Saga

又将這個過程分為了好幾層,我們一層一層來看吧。

runSaga

的參數先是通過

bind

傳入了一些上下文相關的變量,比如

getState, dispatch

,然後又在運作的時候傳入了

rootSaga

,是以他應該是長這個樣子的:

import proc from './proc';

export function runSaga(
  { channel, dispatch, getState },
  saga,
  ...args
) {
  // saga是一個Generator,運作後得到一個疊代器
  const iterator = saga(...args);

  const env = {
    channel,
    dispatch,
    getState,
  };

  proc(env, iterator);
}
           

runSaga

僅僅是将

Generator

運作下,得到疊代器對象後又調用了

proc

來處理。

runSaga

對應的源碼看這裡:https://github.com/redux-saga/redux-saga/blob/master/packages/core/src/internal/runSaga.js

proc

proc

就是具體執行這個疊代器的過程,

Generator

的執行方式我們之前在另一篇文章詳細講過,簡單來說就是可以另外寫一個方法

next

來執行

Generator

next

裡面檢測到如果

Generator

沒有執行完,就繼續執行

next

,然後外層調用一下

next

啟動這個流程就行。

export default function proc(env, iterator) {
  // 調用next啟動疊代器執行
  next();

  // next函數也不複雜
  // 就是執行iterator
  function next(arg, isErr) {
    let result;
    if (isErr) {
      result = iterator.throw(arg);
    } else {
      result = iterator.next(arg);
    }

    // 如果他沒結束,就繼續next
    // digestEffect是處理目前步驟傳回值的函數
    // 繼續執行的next也由他來調用
    if (!result.done) {
      digestEffect(result.value, next)
    }
  }
}
           

digestEffect

上面如果疊代器沒有執行完,我們會将它的值傳給

digestEffect

處理,那麼這裡的

result.value

的值是什麼的呢?回想下我們前面

rootSaga

裡面的用法

yield takeEvery("FETCH_USER_INFO", fetchUserInfo);
           

result.value

的值應該是

yield

後面的值,也就是

takeEvery("FETCH_USER_INFO", fetchUserInfo)

的傳回值,

takeEvery

是再次包裝過的

effect

,他包裝了

take,fork

這些簡單的

effect

。其實對于像

take

這種簡單的

effect

來說,比如:

take("FETCH_USER_INFO", fetchUserInfo);
           

這行代碼的傳回值直接就是一個對象,類似于這樣:

{
  IO: true,
  type: 'TAKE',
  payload: {},
}
           

是以我們這裡

digestEffect

拿到的

result.value

也是這樣的一個對象,這個對象就代表了我們的一個

effect

,是以我們的

digestEffect

就長這樣:

function digestEffect(effect, cb) {    // 這個cb其實就是前面傳進來的next
    // 這個變量是用來解決競争問題的
    let effectSettled;
    function currCb(res, isErr) {
      // 如果已經運作過了,直接return
      if (effectSettled) {
        return
      }

      effectSettled = true;

      cb(res, isErr);
    }

    runEffect(effect, currCb);
  }
           

runEffect

digestEffect

又調用了一個函數

runEffect

,這個函數會處理具體的

effect

:

// runEffect就隻是擷取對應type的處理函數,然後拿來處理目前effect
function runEffect(effect, currCb) {
  if (effect && effect.IO) {
    const effectRunner = effectRunnerMap[effect.type]
    effectRunner(env, effect.payload, currCb);
  } else {
    currCb();
  }
}
           

這點代碼可以看出,

runEffect

也隻是對

effect

進行了檢測,通過他的類型擷取對應的處理函數,然後進行處理,我這裡代碼簡化了,隻支援

IO

這種

effect

,官方源碼中還支援

promise

iterator

,具體的可以看看他的源碼:https://github.com/redux-saga/redux-saga/blob/master/packages/core/src/internal/proc.js

effectRunner

effectRunner

是通過

effect.type

比對出來的具體的

effect

的處理函數,我們先來看兩個:

take

fork

runTakeEffect

take

的處理其實很簡單,就是将它注冊到我們的

channel

裡面就行,是以我們建一個

effectRunnerMap.js

檔案,在裡面添加

take

的處理函數

runTakeEffect

// effectRunnerMap.js

function runTakeEffect(env, { channel = env.channel, pattern }, cb) {
  const matcher = input => input.type === pattern;

  // 注意channel.take的第二個參數是matcher
  // 我們直接寫一個簡單的matcher,就是輸入類型必須跟pattern一樣才行
  // 這裡的pattern就是我們經常用的action名字,比如FETCH_USER_INFO
  // Redux-Saga不僅僅支援這種字元串,還支援多種形式,也可以自定義matcher來解析
  channel.take(cb, matcher);
}

const effectRunnerMap = {
  'TAKE': runTakeEffect,
};

export default effectRunnerMap;
           

注意上面代碼

channel.take(cb, matcher);

cb

,這個

cb

其實就是我們疊代器的

next

,也就是說

take

的回調是疊代器繼續執行,也就是繼續執行下面的代碼。也就是說,當你這樣寫時:

yield take("SOME_ACTION");
yield fork(saga);
           

當運作到

yield take("SOME_ACTION");

這行代碼時,整個疊代器都阻塞了,不會再往下運作。除非你觸發了

SOME_ACTION

,這時候會把

SOME_ACTION

的回調拿出來執行,這個回調就是疊代器的

next

,是以就可以繼續執行下面這行代碼了

yield fork(saga)

runForkEffect

我們前面的示例代碼其實沒有直接用到

fork

這個API,但是用到了

takeEvery

takeEvery

其實是組合

take

fork

來實作的,是以我們先來看看

fork

fork

的使用跟

call

很像,也是可以直接調用傳進來的方法,隻是

call

會等待結果回來才進行下一步,

fork

不會阻塞這個過程,而是目前結果沒回來也會直接運作下一步:

fork(fn, ...args);
           

是以當我們拿到

fork

的時候,處理起來也很簡單,直接調用

proc

處理

fn

就行了,

fn

應該是一個

Generator

函數。

function runForkEffect(env, { fn }, cb) {
  const taskIterator = fn();    // 運作fn得到一個疊代器

  proc(env, taskIterator);      // 直接将taskIterator給proc處理

  cb();      // 直接調用cb,不需要等待proc的結果
}
           

runPutEffect

我們前面的例子還用到了

put

effect

,他就更簡單了,隻是發出一個

action

,事實上他也是調用的

Redux

dispatch

來發出

action

function runPutEffect(env, { action }, cb) {
  const result = env.dispatch(action);     // 直接dispatch(action)

  cb(result);
}
           

注意我們這裡的代碼隻需要

dispatch(action)

就行了,不需要再手動調

channel.put

了,因為我們前面的中間件裡面已經改造了

dispatch

方法了,每次

dispatch

的時候都會自動調用

channel.put

runCallEffect

前面我們發起

API

請求還用到了

call

,一般我們使用

axios

這種庫傳回的都是一個

promise

,是以我們這裡寫一種支援

promise

的情況,當然普通同步函數肯定也是支援的:

function runCallEffect(env, { fn, args }, cb) {
  const result = fn.apply(null, args);

  if (isPromise(result)) {
    return result
      .then(data => cb(data))
      .catch(error => cb(error, true));
  }

  cb(result);
}
           

這些

effect

具體處理的方法對應的源碼都在這個檔案裡面:https://github.com/redux-saga/redux-saga/blob/master/packages/core/src/internal/effectRunnerMap.js

effects

上面我們講了幾個

effect

具體處理的方法,但是這些都不是對外暴露的

effect API

。真正對外暴露的

effect API

還需要單獨寫,他們其實都很簡單,都是傳回一個帶有

type

的簡單對象就行:

const makeEffect = (type, payload) => ({
  IO: true,
  type,
  payload
})

export function take(pattern) {
  return makeEffect('TAKE', { pattern })
}

export function fork(fn) {
  return makeEffect('FORK', { fn })
}

export function call(fn, ...args) {
  return makeEffect('CALL', { fn, args })
}

export function put(action) {
  return makeEffect('PUT', { action })
}
           

可以看到當我們使用

effect

時,他的傳回值就僅僅是一個描述目前任務的對象,這就讓我們的單元測試好寫很多。因為我們的代碼在不同的環境下運作可能會産生不同的結果,特别是這些異步請求,我們寫單元測試時來造這些資料也會很麻煩。但是如果你使用

Redux-Saga

effect

,每次你代碼運作的時候得到的都是一個任務描述對象,這個對象是穩定的,不受運作結果影響,也就不需要針對這個造測試資料了,大大減少了工作量。

effects

對應的源碼檔案看這裡:https://github.com/redux-saga/redux-saga/blob/master/packages/core/src/internal/io.js

takeEvery

我們前面還用到了

takeEvery

來處理同時發起的多個請求,這個

API

是一個進階API,是封裝前面的

take

fork

來實作的,官方源碼又構造了一個新的疊代器來組合他們,不是很直覺。官方文檔中的這種寫法反而很好了解,我這裡采用文檔中的這種寫法:

export function takeEvery(pattern, saga) {
  function* takeEveryHelper() {
    while (true) {
      yield take(pattern);
      yield fork(saga);
    }
  }

  return fork(takeEveryHelper);
}
           

上面這段代碼就很好了解了,我們一個死循環不停的監聽

pattern

,即目标事件,當目标事件過來的時候,就執行對應的

saga

,然後又進入下一次循環繼續監聽

pattern

總結

到這裡我們例子中用到的

API

已經全部自己實作了,我們可以用自己的這個

Redux-Saga

來替換官方的了,隻是我們隻實作了他的一部分功能,還有很多功能沒有實作,不過這已經不妨礙我們了解他的基本原理了。再來回顧下他的主要要點:

  1. Redux-Saga

    其實也是一個釋出訂閱模式,管理事件的地方是

    channel

    ,兩個重點

    API

    take

    put

  2. take

    是注冊一個事件到

    channel

    上,當事件過來時觸發回調,需要注意的是,這裡的回調僅僅是疊代器的

    next

    ,并不是具體響應事件的函數。也就是說

    take

    的意思就是:我在等某某事件,這個事件來之前不許往下走,來了後就可以往下走了。
  3. put

    是發出事件,他是使用

    Redux dispatch

    發出事件的,也就是說

    put

    的事件會被

    Redux

    Redux-Saga

    同時響應。
  4. Redux-Saga

    增強了

    Redux

    dispatch

    函數,在

    dispatch

    的同時會觸發

    channel.put

    ,也就是讓

    Redux-Saga

    也響應回調。
  5. 我們調用的

    effects

    和真正實作功能的函數是分開的,表層調用的

    effects

    隻會傳回一個簡單的對象,這個對象描述了目前任務,他是穩定的,是以基于

    effects

    的單元測試很好寫。
  6. 當拿到

    effects

    傳回的對象後,我們再根據他的

    type

    去找對應的處理函數來進行處理。
  7. Redux-Saga

    都是基于

    Generator

    的,每往下走一步都需要手動調用

    next

    ,這樣當他執行到中途的時候我們可以根據情況不再繼續調用

    next

    ,這其實就相當于将目前任務

    cancel

    了。

參考資料

Redux-Saga

官方文檔:https://redux-saga.js.org/

Redux-Saga

源碼位址: https://github.com/redux-saga/redux-saga/tree/master/packages/core/src

文章的最後,感謝你花費寶貴的時間閱讀本文,如果本文給了你一點點幫助或者啟發,請不要吝啬你的贊和GitHub小星星,你的支援是作者持續創作的動力。

歡迎關注我的公衆号進擊的大前端第一時間擷取高品質原創~

“前端進階知識”系列文章:https://juejin.im/post/5e3ffc85518825494e2772fd

“前端進階知識”系列文章源碼GitHub位址: https://github.com/dennis-jiang/Front-End-Knowledges

手寫Redux-Saga源碼