添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

城市碼農

理解 Redux 原始碼 (1):來實作 createStore 的 getState、dispatch、subscribe 吧

雖然在先前工作中,較常使用 Context API 以及 useReducer 處理狀態管理,但依然很好奇 Redux 是如何實踐「狀態統一控管」及「單向資料流」的概念,加上看過谷哥在 ModernWeb'21 上分享的 挑戰 40 分鐘實作簡易版 Redux 佐設計模式 於是決定閱讀 Redux 原始碼,並實作簡易的 createStore 函式,主要會聚焦在其中的 getState dispatch 以及 subscribe API。

期許閱讀完本文後,能達成:

  • 理解 Redux 是什麼及主要想解決的問題
  • 理解 createStore 中的 getState dispatch subscribe
  • 理解 subscribe 遇到什麼 bugs,如何藉由 currentListners nextListners ensureCanMutateNextListeners 解決
  • 能動手實作基本的 createStore
  • Redux 是什麼?

    在進入實作 Redux createStore 前,先複習 Redux 是什麼及其想解決的問題。

    Redux 是一個基於 Flux 流程概念實踐的集中式資料狀態管理的工具 ,可以使用在 JavaScript 開發的應用程式中,並不限定於 React 或任一框架。

    為什麼會需要這個「集中式」的資料狀態管理工具?

    主因是前端的複雜性越來越高,且時常同類型的資料會散落在不同的區塊元件中, 如果分開管理資料,可能會造成資料狀態不一致的狀況,於是可以透過集中管理資料的方式來解決這個問題

    例如:通常在應用程式中,使用者的資料,如姓名、大頭照、信箱等,會用在不同的區塊元件中,如果沒有集中統一管理資料,可能會造成 A 區塊元件中的信箱資料被更新,但 B 區塊元件中信箱資料卻還是過去的狀態。如果集中管理統一管理資料,亦即使用者的資料來源只有一處,就能解決這個問題。

    可從下圖直觀地了解有統一資料來源(store state)、集中式資料狀態庫的好處。

    除了「集中式」之外,Redux 還有一個關鍵是基於 Flux 實踐的「單向資料流」更新資料方式,簡言之就是 限制更新 store state 的方式,只能透過下圖單向的流程來執行,藉此讓資料的改變更安全、可預期地被控管 ,概念如下圖:

    概要地介紹其中重要的角色:

  • Store :
  • Redux 的核心,可比喻為一個容器,擁有唯一的資料中心 store state 以及會提供 getState dispatch subscribe 等 API 供外部使用。
  • Dispatcher :
  • dispatch 會接收 action 物件, action 通常包含更新資料的方式 action type 及更新資料時所用的值 action payload
  • 如果 store state 發生變化,只有一種可能,就是由 dispatch 派發 action 所觸發的結果。
  • Reducer :
  • 會接收 dispatch 派發的 action ,經由對應 action type 的資料更新規則,進行資料更新後,回傳新的 store state
  • 上面的觀念有個概念即可,記住 Redux 最重要的觀念:

  • 會創建單一的中心資料庫
  • 修改資料的模式是單向資料流
  • 接著將從這兩個觀念延伸,依據 Redux 原始碼的 pattern,實作負責創建 store 的 createStore 函式。

    註:更嚴謹的 Redux 定義,需含 3 個要件 Single source of truth​、State is read-only​(only change by dispatching)、Changes are made with pure functions ,可參考 Redux 文件

    Step 1 : 實作單一資料庫與 getState API

    首先,透過函式模組化 ( Module Pattern ) 的方式,來創建 createStore 並實踐「單一資料庫」的概念:

    /*** createStore.js file ***/
    // 宣告 createStore 函式
    function createStore(preloadedState) {
      // 宣告 currentState 作為單一資料來源,並可初始化為 preloadedState
      let currentState = preloadedState;
      return {};
    export default createStore;
    

    接著創建 getState 函式,並且讓外部能透過 store.getState API 接口使用。當呼叫 getState API 時,會 return currentState

    /*** createStore.js file ***/
    function createStore(preloadedState) {
      let currentState = preloadedState;
      // 創建取得 currentState 的 getState API
      function getState() {
        return currentState;
      // 將內部的 getState function 提供給外部使用
      const store = {
        getState,
      return store;
    export default createStore;
    

    由於 closure 特性,所以在 createStore 中宣告 currentState 變數,不會被 garbage collection 機制回收,因此可以持續存在,提供給外部提取和操作。

    Step 2 : 實作更改資料的 dispatch API

    實踐 store state 以及 getState API 後,下一步就來實踐「更新資料」。

    先在 createState 內部,創建更新資料的方法,稱之為 dispatch,並且能傳入 newState 參數,藉此更新 store state

    /*** createStore.js file ***/
    function createStore(preloadedState) {
      let currentState = preloadedState;
      function getState() {...};
      // 創建更新 store state 的 dispatch API
      function dispatch(newState) {
        // 將 store state 更新為 newState
        currentState = newState;
      const store = {
        getState,
        dispatch,
      return store;
    export default createStore;
    

    看似完成,然而目前更新資料的自由度過高,多人或複雜開發時可能產生非預期問題。

    例如:當 store state 中,原本是 number 型別的資料,會進行數字運算,但開發者不小心使用 dispatch('string') ,將資料更新成 string 時,會造成運算上的問題(Bug)。

    產生 Bug 的範例如下:

    /*** app.js file ***/
    import createStore from './createStore.js';
    const preloadedState = {
      points: 0;
    // 引入剛剛實作的 createStore 創建 store
    const store = createStore(preloadedState);
    // 將 state 加 1
    store.dispatch({
      points: store.getState().points + 1
    // 將 state 減 1
    store.dispatch({
      points: store.getState().points - 1
    // 隨便改,造成後續 Bug,因為將 string 拿去做加減運算爆掉
    store.dispatch({
      points: 'string'
    

    爲了避免上述狀況,會需要有「更新 state 的硬性規則」,分兩個步驟:

  • 需要制定更新 store state 的規則,且確保不會有預期外的 side effect。
  • 需要修改 store.dispatch,讓 dispatch 按造制訂出的規則更新 store state
  • 從第 1 點開始實作,開發者能自定義名為 reducer 的 pure function,預先規範更新 store state 的規則,且由於 reducer 為 pure function 不會有 side effect。

    接著要將定義好的 reducer 傳入 createStore,讓 dispatch 時,可以觸發 reducer,依據制定好的規則更新 store state,藉此避免剛剛提到的非預期問題。

    上述實作如下:

    /*** app.js file ***/
    import createStore from './createStore.js';
    const preloadedState = {
      points: 0;
    // 透過宣告 reducer (pure function),制定修改 state 的規則
    // reducer 會傳入 currentState 及修改的 action 是什麼
    function reducer(state, action) {
      switch (action.type) {
        // 當 action.type 是 INCREMENT 時,執行下面更新資料的邏輯
        case 'INCREMENT':
          return {
            ...state,
            points: state.points + 1;
        // 當 action.type 是 DECREMENT 時,執行下面更新資料的邏輯
        case 'DECREMENT':
          return {
            ...state,
            points: state.points - 1;
        default:
          return state;
    // 將制定好的 reducer 傳入 createStore 中,提供給 dispatch 使用
    const store = createStore(reducer, preloadedState);
    // 僅能透過定義好的 action.type "INCREMENT" 修改 state
    store.dispatch({
      type: 'INCREMENT',
    // 僅能透過制定好的 action.type "DECREMENT" 修改 state
    store.dispatch({
      type: 'DECREMENT'
    

    接續實作第 2 點,優化 createStore 中的 dispatch,使其能依據 reducer 規則修改 store state

    具體而言,就是讓 dispatch 能接收 action,並透過執行 reducer 函式,更新 store state

    /*** createStore.js file ***/
    // 新增 reducer 參數,由外部定義後傳入
    function createStore(reducer, preloadedState) {
      let currentState = preloadedState;
      let currentReducer = reducer;
      function getState() {...};
      // dispatch 可傳入 action object
      // 通常 action object 會有 action.type 與 action.payload
      function dispatch(action) {
        // 透過 reducer 中定義好的規則,更新 store currentState
        currentState = currentReducer(currentState, action)
      const store = {
        getState,
        dispatch,
      return store;
    export default createStore;
    

    至此,就完成更新 store statedispatch。複習一下,它做了兩件事情:

  • 可以接收 action 參數
  • action 傳遞給 reducer,藉此更新 store state
  • Step 3 : 透過 isDispatching 優化 getState 以及 dispatch

    到此為止可思考一件事:當 reducer 更新 state 時,如果再次觸發 getStatedispatch 會不會造成什麼問題?

    換句話說更具體地說:外部使用方能否在自定義的 reducer 內,使用 store.getStatestore.dispatch ?

  • getState : 不必要,因為 reducer 本身首個參數已接收到目前 state,可直接取用,無須呼叫 store.getState
  • dispatch : 不必要,因為預期一個 action 同時只會更動一次 state。而且需要避免 reducer 執行時 action 時,又再度觸發 store.dispatch 執行同個 action 的狀況,這會導致無限遞迴的 Bug。
  • 撇除上面兩項說明,還有個重要思維是 reducer 的本質是專注在接收 action ,並且根據 action type,執行定義好的邏輯去更新 state,更新後回傳。是純 pure function,任何多餘的 side effect 都該避免

    可以透過新增 isDispatching flag,來達成在 reducer 執行中,禁止呼叫 getStatedispatch,流程如下:

  • dispatch 中,reducer 要執行前,將 isDispatching = true
  • 等到 reducer 執行完畢後,把 isDispatching = false
  • 如果在 getStatedispatch 中,遇到 isDispatching = true 就拋出 error message
  • 藉此達成在 reducer 中,無法使用 getStatedispatch 的目標。

    /*** createStore.js file ***/
    function createStore(reducer, preloadedState) {
      let currentState = preloadedState;
      let currentReducer = reducer;
      // 新增 isDispatching flag 去判斷是否正在執行 reducer
      // 在 dispatch 函式執行 reducer 前會轉為 true
      let isDispatching = false;
      function getState() {
        // 在 reducer 內不能使用 store.getState()
        // => isDispatching = true 時要噴錯誤訊息
        if (isDispatching) {
          throw new Error(
            'You may not call store.getState() while the reducer is executing. ' +
              'The reducer has already received the state as an argument. ' +
              'Get the state from the top reducer instead of reading it from the store.'
        return currentState;
      function dispatch(action) {
        // 在 reducer 內不能使用 store.dispatch()
        // => isDispatching = true 時要噴錯誤訊息
        if (isDispatching) {
          throw new Error('Reducers may not dispatch actions when isDispatching.');
        try {
          // 將 isDispatching 轉為 true 並執行 reducer
          isDispatching = true;
          currentState = currentReducer(currentState, action);
        } finally
    
    
    
    
        
     {
          // reducer 執行結束時,將 isDispatching 轉為 false
          isDispatching = false;
      const store = {
        getState,
        dispatch,
      return store;
    export default createStore;
    

    Step 4 : 實作訂閱機制的 subscribe API

    假如有個新需求,希望每當 store state 中的 points 被更新時,要自動 console.log points,在 app.js 中期望的使用方式如下:

    /*** app.js file ***/
    import createStore from './createStore.js';
    ......
    const store = createStore(reducer, preloadedState);
    // 每當 store state 改變時,就執行 callback function,印出 points
    // store 會透過 subscribe 接收這個 callback function 
    store.subscribe(() => {
      console.log(store.getState().points)
    ......
    

    subscribe,需要實踐兩項重要的概念:

  • 可以傳入 callback function 作為訂閱函式
  • store state 改變後,訂閱的 callback function 會被執行
  • 透過下面程式碼,能實作上述概念:

    /*** createStore.js file ***/
    function createStore(reducer, preloadedState) {
      let currentState = preloadedState;
      let currentReducer = reducer;
      let isDispatching = false;
      // 定義 listener,在 subscribe 中被更新 ; 在 dispatch 中被執行
      let listener = null
      function getState() {...};
      function dispatch(action) {
        // 當 state 經由 reducer 更新後,
        // 如果 listener 有已訂閱項目,就會執行 listener
        if(listener) {
          listener();
      // store.subscribe 可傳入 callback function,命名為 listenerCb
      function subscribe(listenerCb) {
        // 在 reducer 執行時,不能新增訂閱
        if (isDispatching) {
            throw new Error(
              "You may not call store.subscribe() while the reducer is executing. " +
                "If you would like to be notified after the store has been updated, " +
                "subscribe from a component and invoke store.getState() in the callback to access the latest state."
        // 訂閱 listenerCb
        listener = listenerCb;
      const store = {
        getState,
        dispatch,
        subscribe,
      return store;
    export default createStore;
    

    接續以需求面,思考兩個問題:

  • 是否有需要取消訂閱的時候?會,所以需要有 unsubscribe 的方法。
  • 是否有需要訂閱多個事件(callback)的時候?會,所以需要 listeners array 而非 listener 而已。
  • 先處理第 1 點「取消訂閱」的需求,讓 subscribe 會 return unsubscribe,如此一來就能透過執行 unsubscribe() 來取消訂閱。

    /*** createStore.js file ***/
    function createStore(reducer, preloadedState) {
      function subscribe(listenerCb) {
        if (isDispatching) {...}
        // 新增 isSubscribe flag 判斷 unsubscribe 是否要被執行
        let isSubscribed = true;
        listener = listenerCb;
        // 新增 unsubscribe 作為取消訂閱時使用
        return unsubscribe () {
          if (!isSubscribed) {
              return;
          if (isDispatching) {
              throw new Error(
                "You may not unsubscribe from a store listener while the reducer is executing."
          // unsubscribe 被執行時,listener 改回 null,藉此移除訂閱者
          listener = null;
          isSubscribed = false;
      const store = {
        getState,
        dispatch,
        subscribe,
      return store;
    export default createStore;
    

    接著處理第 2 個需求「訂閱多個 callback 事件」:

    /*** createStore.js file ***/
    function createStore(reducer, preloadedState) {
      // 將 listener 改為 listeners = []
      let listeners = [];
      function getState() {...};
      function dispatch(action) {
        // 在 state 更新後,執行所有訂閱的 listener 事件
        for(let i = 0; i < listeners.length; i++) {
          const listener = listeners[i];
          listener();
      function subscribe(listener) {
        // 新訂閱的 listener,會被加入 listeners
        listeners.push(listener);
        return unsubscribe () {
          // 移除訂閱的事件,會被抽離 listeners
          const index = listeners.indexOf(listener);
          listeners.splice(index, 1);
          isSubscribed = false;
      const store = {
        getState,
        dispatch,
        subscribe,
      return store;
    export default createStore;
    

    到此就完成 subscribe/unsubscribe 基本功能了。

    Step 5 : 修復多層 subscribe / unsubscribe 的問題

    目前實作的 Redux 在開發端執行多層 subscribe/unsubscribe 時會有問題,如下:

    /*** app.js ***/
    const store = createStore(reducer, preloadedState);
    const unsubscribe1 = store.subscribe(() => {...});
    const unsubscribe2 = store.subscribe(() => {
      // 在 subscribe callback 中執行 unsubscribe 會有問題
      unsubscribe1();
      // 在 subscribe callback 中執行另一個 subscribe 也有問題
      const unsubscribe3 = store.subscribe(() => {...});
    

    為什麼上面這樣會有問題?

    因為在針對 listeners array 進行 for loop 時,更改到 listeners 的長度,所以會造成非預期的執行狀況

    /*** createStore.js file ***/
    function createStore(reducer, preloadedState) {
      function dispatch(action) {
        for(let i = 0; i < listeners.length; i++) {
          const listener = listeners[i];
          // 假設在這個 listener 執行時,改變 listeners 的長度
          // 就可能導致有項目被跳過沒有被執行,或是非預期地多被執行
          listener();
    export default createStore;
    

    處理此問題,可以透過確保正在執行的 listeners 不會被 subscribe / unsubscribe 影響

    透過創建 currentListenersnextListeners 達成這個目的:

  • currentListeners : stable, 正在被 for 迴圈執行的 listeners
  • nextListeners : unstable, 會被 subscribeunsubscribe 改變的 listeners
  • /*** createStore.js file ***/
    function createStore(reducer, preloadedState) {
      // 將 listeners 改為 currentListeners 及 nextListeners
      let currentListeners = [];
      let nextListeners = currentListeners;
      function getState() {...};
      function dispatch(action) {
        // 在執行 currentListeners 前,先將 currentListeners 更新成最新的 listeners
        const listeners = (currentListeners = nextListeners);
        for (let i = 0; i < listeners.length; i++) {
          const listener = listeners[i];
          listener();
      function subscribe(listener) {
        // subscribe 時,改為對 nextListeners 進行操作
        nextListeners.push(listener);
        return unsubscribe () {
          // unsubscribe 時,改為對 nextListeners 進行操作
          const index = nextListeners.indexOf(listener);
          nextListeners.splice(index, 1);
          isSubscribed = false;
      const store = {
        getState,
        dispatch,
        subscribe,
      return store;
    export default createStore;
    

    特別注意的是 array 為 object data 複製時是複製 reference 而非 value,所以新增 ensureCanMutateNextListeners 處理這個問題。

    /*** createStore.js file ***/
    function createStore(reducer, preloadedState) {
      let currentListeners = [];
      let nextListeners = currentListeners;
      function getState() {...};
      // 利用淺拷貝,確保 nextListeners 與 currentListeners 不會指向同一個資料
      function ensureCanMutateNextListeners() {
        if (nextListeners === currentListeners) {
          nextListeners = currentListeners.slice();
      function dispatch(action) {
        // 在執行 currentListeners 前,先將 currentListeners 更新成最新的 listeners
        const listeners = (currentListeners = nextListeners);
        for (let i = 0; i < listeners.length; i++) {
          const listener = listeners[i];
          listener();
      function subscribe(listener) {
        // 先執行 ensureCanMutateNextListeners,
        // 確保對 nextListeners 操作,絕不會改到 currentListeners
        ensureCanMutateNextListeners();
        nextListeners.push(listener);
        return unsubscribe () {
          // 先執行 ensureCanMutateNextListeners,
          // 確保對 nextListeners 操作,絕不會改到 currentListeners
          ensureCanMutateNextListeners();
          const index = nextListeners.indexOf(listener);
          nextListeners.splice(index, 1);
          isSubscribed = false;
      const store = {
        getState,
        dispatch,
        subscribe,
      return store;
    export default createStore;
    

    經過以上的處理,才算完整地實作 subscribe/unsubscribe

    Step 6 : 添加初始化的 dispatch

    最後一步,來到最後一步了!

    添加初始化用 dispatch,讓最初的 state 返回 reducer 中所設定的 initialState

    /*** createStore.js file ***/
    function createStore(reducer, preloadedState) {
      // 給予一串隨機的字串
      const randomString = () =>
        Math.random().toString(36).substring(7).split("").join(".");
      // 初始化 state 的 dispatch
      // 利用 randomString 避免與使用者自定義的 INIT action type 衝突
      dispatch({
        type: `INIT${randomString()}`,
      const store = {
        getState,
        dispatch,
        subscribe,
      return store;
    export default createStore;
    

    至此就完成 Redux 核心的 createStore!

    回顧整個 createStore 程式碼

    整段程式碼如下,附上註解解釋,如需無註解的版本,可以點此到 Github 上觀看

    /*** createStore.js file ***/
    function createStore(reducer, preloadedState) {
      // currentState 就是核心的 store state,初始的 preloadedState 由外部傳入
      let currentState = preloadedState;
      // currentReducer 就是更新 state 會使用的 reducer,由外部傳入
      let currentReducer = reducer;
      // currentListeners 以及 nextListeners 是為了 subscribe 功能而設計
      let currentListeners = [];
      let nextListeners = currentListeners;
      // isDispatching flag 是為了避免 reducer 中使用 getState、dispatch、subscribe 而設計
      let isDispatching = false;
      // ensureCanMutateNextListeners 利用淺拷貝,確保 nextListeners 以及 currentListeners 第一層指向不同來源
      function ensureCanMutateNextListeners() {
        if (nextListeners === currentListeners) {
          nextListeners = currentListeners.slice();
      // 外部可透過 store.getState 取得 store state
      function getState() {
        if (isDispatching) {
          throw new Error(
            'You may not call store.getState() while the reducer is executing. ' +
              'The reducer has already received the state as an argument. ' +
              'Get the state from the top reducer instead of reading it from the store.'
        return currentState;
      // 外部透過 store.dispatch(action) 改變 store state
      function dispatch(action) {
        if (isDispatching) {
          throw new Error('Reducers may not dispatch actions when isDispatching.');
        try {
          isDispatching = true;
          // 透過 currentReducer 的執行,返回更新後的 store state
          currentState = currentReducer(currentState, action);
        } finally {
          isDispatching = false;
        // store state 更新後,先更新 currentListeners,接著觸發所有訂閱的 listener
        const listeners = (currentListeners = nextListeners);
        for (let i = 0; i < listeners.length; i++) {
          const listener = listeners[i];
          listener();
      // 外部透過 store.subscribe(listener) 訂閱、 unsubscribe(listener) 取消訂閱 listener
      function subscribe(listener) {
        if (isDispatching) {
          throw new Error(
            'You may not call store.subscribe() while the reducer is executing. ' +
              'If you would like to be notified after the store has been updated, ' +
              'subscribe from a component and invoke store.getState() in the callback to access the latest state.'
        let isSubscribed = true;
        // 將新的 listener 添加到 nextListeners 前,先確保 currentListeners 與 nextListeners 不同
        ensureCanMutateNextListeners();
        nextListeners.push(listener);
        return function unsubscribe(listener) {
          if (!isSubscribed) {
            return;
          if (isDispatching) {
            throw new Error(
              'You may not unsubscribe from a store listener while the reducer is executing. '
          // 將 listener 從 nextListeners 移除前,先確保 currentListeners 與 nextListeners 不同
          ensureCanMutateNextListeners();
          const index = nextListeners.indexOf(listener);
          nextListeners.splice(index, 1);
          isSubscribed = false;
      const randomString = () => Math.random().toString(36).substring(7).split('').join('.');
      // initialize,透過 randomString() 避免與外部使用者定義的 INIT action type 撞名
      dispatch({
        type: `INIT${randomString()}`,
      const store = {
        getState,
        dispatch,
        subscribe,
      return store;
    export default createStore;
    

    同時附上使用 createStore 的範例 app.js 檔案:

    /*** app.js file ***/
    import { createStore } from './createStore.js';
    // 自定義 reducer
    const reducer = (state, action) => {
      switch (action.type) {
        // 如果接收到 PLUS_POINTS 的 action.type,就增加 points
        case 'PLUS_POINTS':
          return {
            points: state.points + action.payload,
        // 如果接收到 MINUS_POINTS 的 action.type,就減少 points
        case 'MINUS_POINTS':
          return {
            points: state.points - action.payload,
        default:
          return state;
    // 將自定義的 reducer 傳入 createStore 中,藉此創建 store
    // store 會提供 getState、dispatch、subscribe API
    const preloadedState = {
      points: 0,
    const store = createStore(reducer, preloadedState);
    // 當 plus 按鈕被點擊時,就觸發 callback,增加 100 points
    document.getElementById('plus-points-btn').addEventListener('click', () => {
      // 透過 dispatch { type: 'PLUS_POINTS', payload: 100 }
      // 將 store state 中的 points 增加 100
      store.dispatch({
        type: 'PLUS_POINTS',
        payload: 100,
    // 當 minus 按鈕被點擊時,就觸發 callback,減少 100 points
    document.getElementById('minus-points-btn').addEventListener('click', () => {
      // 透過 dispatch { type: 'MINUS_POINTS', payload: 100 }
      // 將 store state 中的 points 減少 100
      store.dispatch({
        type: 'MINUS_POINTS',
        payload: 100,
    // 透過 subscribe 訂閱機制,當資料被更新時,就會執行傳入的 callback
    store.subscribe(() => {
      // 透過 getState 取出最新的 points 並 render 到畫面上
      const points = store.getState().points;
      document.getElementById('display-points-automatically').textContent = points;
    

    回顧閱讀文章後要達成的目標

    來回文章最初幾個希望閱讀後,能達成的項目:

    1. 理解 Redux 是什麼,以及主要想解決的問題

    Redux 是一個基於 Flux 流程概念實踐的「集中式」資料狀態管理的工具,最主要的目的是統一管理資料,避免資料狀態不一致的問題,且也利用「單向資料流」的方式控管資料狀態,讓資料變動更可預期與維護。

    2. 理解並實作 createStore 中的 getState、dispatch、subscribe

    createStore 的核心在於單一控管的 sore state,且提供下列三個 API :

  • getState : 取得目前的 store state
  • dispatch : 透過傳入 action (含 type and payload) 更新 store state
  • subscribe : 透過傳入 callback,就能訂閱 callback,在 sotore state 更新後會執行 callback。
  • 3. 理解 subscribe 會遇到什麼 bugs,如何藉由 currentListners、nextListners、ensureCanMutateNextListeners 解決

    如果不特別處理,在 subscribe 傳入的 listner callback 中執行另一個 subscribeunsubscribe 可能遇到非預期 Bugs。

    解決方案的關鍵是:

  • currentListners : 創建 currentListners,stable,在 state 變動後,會真的執行其中每個 listner
  • nextListners : 創建 nextListners,unstable,當 subscribe/unsubscribe 時,會在 nextListners 中新增或移除 listner
  • ensureCanMutateNextListeners : 因為 listners 是 array,為確保 currentListnersnextListners 不同,因此在 nextListners 操作前,會先執行 ensureCanMutateNextListeners
  • 4. 能動手實作 basic createStore function

    非常推薦可以自己實作,印象會更深刻!如果卡住,再隨時回顧本文或 Redux 的原始碼。

    雖然此篇文章尚未做出完整的 createStore,像是沒實作 enhancer 相關功能,但透過實作 getStatedispatchsubscribe,已能理解核心的 Redux 運作,也知道它是如何透過 closure、listeners 等模式,去封裝並實踐集中式資料管理以及監聽資料變化等概念,非常有趣。

    如果對 enhancermiddlewares 機制有興趣,歡迎閱讀下篇文章:理解 Redux 原始碼 (2):來實作 middlewares、applyMiddleware 以及 createStore enhancer 吧

  • LiangYingC/understand-redux-source-code
  • reduxjs/redux | redux source code
  •