redux 原始碼解析與實際應用

jserTang發表於2018-11-15

Redux

對於 Redux 結構與程式碼實現的剖析,以及專案中的高階用法,不進行對於API的介紹

createStore

  • createStore是一個函式,接收三個引數recdcer,initState,enhancer
    • enhancer是一個高階函式,用於增強create出來的store,他的引數是createStore,返回一個更強大的store生成函式。(功能類似於middleware)。
    • 我們mobile倉庫中的storeCreator其實就可以看成是一個enhancer,在createStore的時候將saga揉入了進去只不過不是作為createStore的第三個引數完成,而是使用middleware完成。
    function createStore(reducer, preloadedState, enhancer) {
      if (typeof enhancer !== 'undefined') {
      // createStore 作為enhancer的引數,返回一個被加強的createStore,然後再將reducer, preloadedState傳進去生成store
        return enhancer(createStore)(reducer, preloadedState);
      }
      // ......
      return {
        dispatch: dispatch,
        subscribe: subscribe,
        getState: getState,
        replaceReducer: replaceReducer
      };
    }
    
    複製程式碼
  • applyMiddleware 與 enhancer的關係。
    • 首先他們兩個的功能一樣,都是為了增強store
    • applyMiddleware的結果,其實就是一個enhancer
function applyMiddleware() {
  // 將傳入的中介軟體放入middlewares
  for (var _len = arguments.length, middlewares = Array(_len), _key = 0; _key < _len; _key++) {
    middlewares[_key] = arguments[_key];
  }
  // return了一個 enhancer函式,引數為createStore,內部對store進行了增強
  return function (createStore) {
    return function () {
      // 將createStore的引數傳入createStore,並生成store
      var store = createStore.apply(undefined, args);
      
      // 增強 dispatch
      var _dispatch = compose.apply(undefined, chain)(store.dispatch);

      // return 一個被增強了dispatch的store
      return _extends({}, store, {
        dispatch: _dispatch
      });
    };
  };
}

複製程式碼

store

store有四個基礎方法: dispatch、subscribe、getState、replaceReducer

  1. store.dispatch (發起action)
function dispatch(action) {
  // 校驗 action 格式是否合法
  if (typeof action.type === 'undefined') {
    throw new Error('action 必須有type屬性');
  }

  // 不可以在 reducer 進行中發起 dispatch
  if (isDispatching) {
    throw new Error('Reducers may not dispatch actions.');
  }

  try {
    // 標記 dispatch 狀態
    isDispatching = true;

    // 執行相應的 reducer 並獲取新更新的 state
    currentState = currentReducer(currentState, action);
  } finally {
    isDispatching = false;
  }
  // 把上次subscribe時得到的新的監聽函式列表,賦值成為當前的監聽函式列表
  var listeners = currentListeners = nextListeners;
  // dispatch 的時候會依次執行 nextListeners 的監聽函式
  for (var i = 0; i < listeners.length; i++) {
    var listener = listeners[i];
    listener();
  }

  return action;
}
複製程式碼
  1. store.subscribe (用於監聽 store 的變化)
function subscribe(listener) {
    // 如果是在 dispatch時註冊subscribe,丟擲警告
    if (isDispatching) {
      throw new Error('......');
    }
    // 將監聽函式放入一個佇列 
    nextListeners.push(listener);
    // return 一個函式,用於登出監聽事件
    return function unsubscribe() {
        // 同樣的,不能再 dispatch 時進行登出操作
        if (isDispatching) {
          throw new Error('......');
        }

        var index = nextListeners.indexOf(listener);
        nextListeners.splice(index, 1);
    };
}
複製程式碼
  1. store.getState (獲取當前的state)
function getState() {
    if (isDispatching) {
      throw new Error('不允許在reducer執行中獲取state');
    }
    // retuen 上次 dispatch 時所更新的 currentState
    return currentState;
}
複製程式碼
  1. store.replaceReducer (提換當前的reducer)
function replaceReducer(nextReducer) {
    // 檢驗新的 reducer 是否是一個函式
    if (typeof nextReducer !== 'function') {
      throw new Error('Expected the nextReducer to be a function.');
    }
    // 替換掉當前的 reducer
    currentReducer = nextReducer;
    // 發起一次新的 action, 這樣可以使 sisteners 函式列表執行一遍,也可以更新一遍 currentState
    dispatch({ type: ActionTypes.REPLACE });
}
複製程式碼

combineReducers(組合reducer)

用於將多個reducer組合成一個reducer,接受一個物件,物件的每個屬性即是單個reducer,各個reducer的key需要和傳入該reducer的state引數同名。

function combineReducers(reducers) {
  // 所有傳入 reducers 的 key
  var reducerKeys = Object.keys(reducers);
  var finalReducers = {};

  // 遍歷reducerKeys,將合法的 reducers 放入 finalReducers
  for (var i = 0; i < reducerKeys.length; i++) {
    var key = reducerKeys[i];

    if (typeof reducers[key] === 'function') {
      finalReducers[key] = reducers[key];
    }
  }
  // 可用的 reducers的 key
  var finalReducerKeys = Object.keys(finalReducers);

  var unexpectedKeyCache = void 0;
  {
    unexpectedKeyCache = {};
  }

  var shapeAssertionError = void 0;

  // 將每個 reducer 都執行一遍,檢驗返回的 state 是否有為undefined的情況
  try {
    assertReducerShape(finalReducers);
  } catch (e) {
    shapeAssertionError = e;
  }

  // return 一個組合過的 reducer 函式,返回值為 state 是否有變化
  return function combination() {
    var state = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
    var action = arguments[1];

    // 如果有返回的state不合法的reducer,丟擲錯誤
    if (shapeAssertionError) {
      throw shapeAssertionError;
    }

    {
      // 校驗 state 與 finalReducers 的合法性
      var warningMessage = getUnexpectedStateShapeWarningMessage(state, finalReducers, action, unexpectedKeyCache);
      if (warningMessage) {
        warning(warningMessage);
      }
    }

    var hasChanged = false;
    var nextState = {};
    // 遍歷所有可用的reducer,將reducer的key所對應的state,代入到reducer中呼叫
    for (var _i = 0; _i < finalReducerKeys.length; _i++) {
      var _key = finalReducerKeys[_i];
      var reducer = finalReducers[_key];
      // reducer key 所對應的 state,這也是為什麼 reducer 名字要與 state 名字相對應的原因
      var previousStateForKey = state[_key];
      // 呼叫 reducer
      var nextStateForKey = reducer(previousStateForKey, action);
      // reducer 返回了新的 state,呼叫store.getState時返回的就是他
      nextState[_key] = nextStateForKey;
      // 新舊 state 是否有變化 ?
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey;
    }
    return hasChanged ? nextState : state;
  };
}
複製程式碼

bindActionCreators

其實就是改變action發起的方式,之前是dispatch的方式,用bindActionCreators將actionCreator包裝後,生成一個key為actionType,value為接受 payload 的函式的物件,發起action的時候直接呼叫這裡面名為跟action的type同名的函式

  • 它的核心其實就是將actionCreator傳入然後返回一個可以發起dispatch的函式,函式中的dispatch接受一個已經生成的action,和在使用它的時候傳入的playload

function bindActionCreator(actionCreator, dispatch) {
  return function () {
    return dispatch(actionCreator.apply(this, arguments));
  };
}
複製程式碼
  • 將多個 actionCreators 進行包裝,最終返回一個被包裝過的actionCreators
function bindActionCreators(actionCreators, dispatch) {

  // 如果傳入一個函式,說明只有一個,actionCreator,返回一個可以進行 dispatch 的函式
  if (typeof actionCreators === 'function') {
    return bindActionCreator(actionCreators, dispatch);
  }

  if ((typeof actionCreators === 'undefined' ? 'undefined' : _typeof(actionCreators)) !== 'object' || actionCreators === null) {
    throw new Error('校驗actionCreators是否是物件');
  }
  // 檢索出 actionCreators 的 key
  var keys = Object.keys(actionCreators);
  var boundActionCreators = {};
  // 迴圈將 actionCreators 中的項用 bindActionCreator 包裝一遍,放入 boundActionCreators 物件中並return
  for (var i = 0; i < keys.length; i++) {
    var key = keys[i];
    var actionCreator = actionCreators[key];
    if (typeof actionCreator === 'function') {
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch);
    }
  }
  return boundActionCreators;
}
複製程式碼

compose

將多個函式組合成一個,從右往左依次執行

function compose() {
  // 獲取傳入引數的對映
  for (var _len = arguments.length, funcs = Array(_len), _key = 0; _key < _len; _key++) {
    funcs[_key] = arguments[_key];
  }
  // 如果引數為0,return 一個 所傳即所得的函式
  if (funcs.length === 0) {
    return function (arg) {
      return arg;
    };
  }
  // 如果只有一個,返回此引數
  if (funcs.length === 1) {
    return funcs[0];
  }
  // 使用 reduce 將所有傳入的函式組合為一個函式,每一次執行reduce,a作為前一個函式都會被這個return的函式重新賦值
  return funcs.reduce(function (a, b) {
    // 每次執行 reduce 都會返回這個函式,這個函式裡返回的前一個函式接受下一個函式的返回值作為引數
    return function () {
      return a(b.apply(undefined, arguments));
    };
  });
}
複製程式碼

applyMiddleware (增強dispatch)

其實applyMiddleware就是將傳入的中介軟體進行組合,生成了一個接受 createStore為引數的函式(enhancer)。

// applyMiddleware將傳入的中介軟體組合成一個enhancer
// 然後再傳入createStore改造成一個增強版的createStore
// 最後傳入reducer 和 initialState 生成 store。
const store = applyMiddleware(...middlewares)(createStore)(reducer, initialState);

// 其實跟這樣寫沒什麼區別
const store = createStore(reducer, initialState, applyMiddleware(...middlewares));

複製程式碼
  • 程式碼分析
function applyMiddleware() {
  // 將傳入的中介軟體組合成一個陣列
  for (var _len = arguments.length, middlewares = Array(_len), _key = 0; _key < _len; _key++) {
    middlewares[_key] = arguments[_key];
  }
  // 返回一個接受 createStore 為引數的函式,也就是 enhancer
  return function (createStore) {
    // 其實這就是最終返回的被增強的 createStore
    return function () {
      for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
        args[_key2] = arguments[_key2];
      }
      // 生成一個 store
      var store = createStore.apply(undefined, args);
      // 宣告一個_dispatch,用於替換 store 的 dispatch
      var _dispatch = function dispatch() {
        throw new Error('不允許在構建中介軟體時進行排程');
      };

      // 返回一個middlewareAPI,下一步將會被帶入中介軟體,使得每一箇中介軟體中都會有 getState 與 dispatch (例如redux-thunk)
      // 這裡面的 dispatch中,將會執行_dispatch(被增強的dispatch)
      var middlewareAPI = {
        getState: store.getState,
        dispatch: function dispatch() {
          return _dispatch.apply(undefined, arguments);
        }
      };
      // 每一箇中介軟體都執行一遍 middlewareAPI
      var chain = middlewares.map(function (middleware) {
        return middleware(middlewareAPI);
      });
      // 將 chain 用compose進行組合,所以傳入的中介軟體依賴必須是倒序的
      // 並傳入 store.dispatch,生成一個被增強的 dispatch
      _dispatch = compose.apply(undefined, chain)(store.dispatch);
      // 生成 store, 使用 _dispatch 替換 store 原始的 dispatch
      return _extends({}, store, {
        dispatch: _dispatch
      });
    };
  };
}
複製程式碼

結合中介軟體redux-thunk感受一下applyMiddleware

redux-thunk 可以使dispatch接受一個函式,以便於進行非同步操作

import { createStore, applyMiddleware } from 'redux';
import reduxThunk from 'redux-thunk';
import reducer from './reducers';

const store = createStore(
  reducer,
  {},
  applyMiddleware(reduxThunk),
);
複製程式碼
  • 原始碼
function createThunkMiddleware(extraArgument) {
  // reuturn 一個接受dispatch, getState的函式,
  // 這個函式返回的函式又接受上一個中介軟體的返回值,也就是被上一個中介軟體包裝過的dispatch
  // 如果接受的action是個函式,那麼就將dispatch, getState傳進去
  return ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }
    return next(action);
  };
}

const thunk = createThunkMiddleware();

export default thunk;
複製程式碼
  • 如果把thunk跟applyMiddleware組裝起來,就是這樣的
function applyMiddleware() {
  ...
  var middlewareAPI = {
        getState: store.getState,
        dispatch: function dispatch() {
          return _dispatch.apply(undefined, arguments);
        }
  };
  var chain = middlewares.map(function () {
        // 這是middleware將middlewareAPI傳進去後return的函式
        return function(next) {
            return function(action) {
              if (typeof action === 'function') {
                return action(dispatch, getState);
              }
              return next(action);
            }
        }
  });
  // 將store.dispatch,也就是next傳進去
  _dispatch = compose.apply(undefined, chain)(store.dispatch);
}
複製程式碼

react-redux

用於繫結react 與 redux,其主要提供了兩個功能

Provider

用於包裝元件樹,將store傳入context中,使其子節點都可以拿到store,不需要一級一級的往下傳。

class Provider extends Component {
    // 將 store 放入 context 中
    getChildContext() {
         return { store: this.store}
    }

    constructor(props, context) {
        super(props, context)
        this.store = props.store;
    }

    render() {
        return Children.only(this.props.children)
    }
}
複製程式碼

connect

connect 用於state與容器元件之間的繫結。
connect 接受三個引數 mapStateToProps, mapDispatchToProps, mergeProps 用於定義需要傳入容器元件的state與dispatch,然後return一個接受容器元件的函式(高階元件),這個高階函式會對將組合好的props混入進容器元件。

var containerComponent = connect(mapStateToProps,mapDispatchToProps)(someComponent); 
ReactDOM.render(
    <Provider store={store}>
        <HashRouter>
            <div>
                <Route exact path="/" component={containerComponent} />
            </div>
        </HashRouter>
    </Provider>,
    document.querySelector('.doc')
);
複製程式碼
  • connect 接收的引數
    • mapStateToProps 返回需要傳入容器元件的 state 的函式
    const mapStateToProps = (state) => {
        return {
            stateName: state[stateName],
        };
    }
    複製程式碼
    • mapDispatchToProps 返回需要傳入容器元件dispatch的函式 or 物件(如果是物件的話傳入的需要是個actionCreator,因為connect 內會用 bindActionCreators 將這個物件包裝)
    // mapDispatchToProps 是個函式
    const mapDispatchToProps = (dispatch) => {
        return {
            dispatchName: (action) => {
                dispatch(action);
            },
        };
    }
    
    // or 當 mapDispatchToProps 是個物件時原始碼中的處理
    export function whenMapDispatchToPropsIsObject(mapDispatchToProps) {
      return (mapDispatchToProps && typeof mapDispatchToProps === 'object')
          ? wrapMapToPropsConstant(dispatch => bindActionCreators(mapDispatchToProps, dispatch))
          : undefined
    }
    複製程式碼
    • mergeProps 規定容器元件props合併方式的函式
    // 預設是將 mapStateToProps, mapDispatchToProps 與元件自身的props進行 merge
    const mergeProps = (stateProps, dispatchProps, ownProps) => {
        return { ...ownProps, ...stateProps, ...dispatchProps };
    }
    複製程式碼
  • connect 內的核心函式
    • finalPropsSelectorFactory(dispatch, {...options})

      return 一個pureFinalPropsSelector函式,這個函式接受兩個引數,(state, props)並返回一個 mergeProps, 他將會在高階元件wrapWithConnect中使用並傳入store.getState()和props,並以此對比當前的 props 以決定在 shouldComponentUpdate 時是否需要更新

    • connectAdvanced(finalPropsSelectorFactory)

      return 一個接受 容器元件為引數的高階元件(wrapWithConnect)。 wrapWithConnect需要的變數與屬性,這也就是connect最終 return 的結果。

    • wrapWithConnect(WrappedComponent)

      這個就是被 connectAdvanced 返回的高階元件,其接受一個容器元件作為引數,在內部建立一個 Connect 元件並在 render 的時候將整合好的 props 傳入 容器元件。

    • hoistStatics(a, b) 將 b 的屬性複製到 a

      用於在包裝元件的時候,將傳入的容器元件內的屬性都複製到 Connect 元件

      function hoistNonReactStatics(targetComponent, sourceComponent, blacklist) {
          // 如果傳入的 b 是字串,直接return a
          if (typeof sourceComponent !== 'string') {
              // 層層遞迴,直到拿到 sourceComponent 的建構函式
              var inheritedComponent = Object.getPrototypeOf(sourceComponent);
              if (inheritedComponent && inheritedComponent !== Object.getPrototypeOf(Object)) {
                  hoistNonReactStatics(targetComponent, inheritedComponent, blacklist);
              }
      
              // b 的所有自有屬性的 key ,包括 Symbols 屬性
              var keys = Object.getOwnPropertyNames(sourceComponent);
              keys = keys.concat(Object.getOwnPropertySymbols(sourceComponent));
      
              // 過濾掉某些屬性,並將 b 的屬性複製給 a
              for (var i = 0; i < keys.length; ++i) {
                  var key = keys[i];
                  if (!REACT_STATICS[key] && !KNOWN_STATICS[key] && (!blacklist || !blacklist[key])) {
                      var descriptor = Object.getOwnPropertyDescriptor(sourceComponent, key);
                      Object.defineProperty(targetComponent, key, descriptor);
                  }
              }
              // return 一個被新增了 b 的屬性的 a
              return targetComponent;
          }
          return targetComponent;
      }
      複製程式碼
  • connect 原始碼分析
function connect( mapStateToProps, mapDispatchToProps, mergeProps){
    // 對傳入的引數進行型別校驗 與 封裝
    const initMapStateToProps = match(mapStateToProps, defaultMapStateToPropsFactories, 'mapStateToProps')
    const initMapDispatchToProps = match(mapDispatchToProps, defaultMapDispatchToPropsFactories, 'mapDispatchToProps')
    const initMergeProps = match(mergeProps, defaultMergePropsFactories, 'mergeProps')
    
    // return 一個接受 容器元件 為引數的高階元件(wrapWithConnect)
    return connectAdvanced(finalPropsSelectorFactory)
}

// 接受的其實是 `finalPropsSelectorFactory`
function connectAdvanced(selectorFactory) {

    const storeKey = 'store';
    // 用於說明訂閱物件
    const subscriptionKey = storeKey + 'Subscription';
    // 定義 contextTypes 與 childContextTypes 用於返回的高階函式裡的包裝元件 Connect
    const contextTypes = {
      [storeKey]: storeShape,
      [subscriptionKey]: subscriptionShape,
    }
    const childContextTypes = {
      [subscriptionKey]: subscriptionShape,
    }
    
    // 返回一個高階元件
    return function wrapWithConnect(WrappedComponent) {
        // 這是一個接受真假 與 提示語 並丟擲錯誤的方法,這裡用來校驗傳入的是否是個函式
        invariant(typeof WrappedComponent == 'function', `You must pass a component to the function`)
        // 將要傳入 finalPropsSelectorFactory 的 option
        const selectorFactoryOptions = {
            getDisplayName: name => `ConnectAdvanced(${name})`,
            methodName: 'connectAdvanced',
            renderCountProp: undefined,
            shouldHandleStateChanges: true,
            storeKey: 'store',
            withRef: false,
            displayName: getDisplayName(WrappedComponent.name),
            wrappedComponentName:  WrappedComponent.displayName || WrappedComponent.name,
            WrappedComponent
        }
        
        // 用於生成一個 selector,用於Connect元件內部的更新控制
        function makeSelectorStateful(sourceSelector, store) {
            // wrap the selector in an object that tracks its results between runs.
            const selector = {
                // 比較 state 與 當前的selector的props,並更新selector
                // selector 有三個屬性:
                // shouldComponentUpdate: 是否允許元件更新更新
                // props: 將要更新的props
                // error: catch 中的錯誤
                run: function runComponentSelector(props) {
                    try {
                        const nextProps = sourceSelector(store.getState(), props)
                        if (nextProps !== selector.props || selector.error) {
                            selector.shouldComponentUpdate = true
                            selector.props = nextProps
                            selector.error = null
                        }
                    } catch (error) {
                        selector.shouldComponentUpdate = true
                        selector.error = error
                    }
                }
            }

            return selector
        }
        
        // 最終 return 的元件,用於包裝傳入的WrappedComponent
        class Connect extends Component {
            constructor(props, context) {
                super(props, context)
                this.store = props['store'] || context['store']
                this.propsMode = Boolean(props['store'])
                
                // 校驗是否傳入了 store
                invariant(this.store, `Could not find store in either the context')
                       
                this.initSelector()
                this.initSubscription()
            }
            
            componentDidMount() {
                // 會把 onStateChange 掛載到對store的訂閱裡
                // 內部呼叫了 store.subscribe(this.onStateChange)
                this.subscription.trySubscribe()
                // 更新一遍 props
                this.selector.run(this.props)
                if (this.selector.shouldComponentUpdate) this.forceUpdate()
            }
            
            // 每次更新 props 都去對比一遍 props
            componentWillReceiveProps(nextProps) {
                this.selector.run(nextProps)
            }
            
            // 根據 selector 來進行元件更新的控制
            shouldComponentUpdate() {
                return this.selector.shouldComponentUpdate
            }
            
            // 初始化 selector,用於元件props更新的控制
            initSelector() {
                // 用於比較state與props的函式。並返回 merge 後的props
                const sourceSelector = selectorFactory(this.store.dispatch, selectorFactoryOptions)
                this.selector = makeSelectorStateful(sourceSelector, this.store)
                this.selector.run(this.props)
            }

            // 初始化訂閱模型: this.subscription
            initSubscription() {
                // 定義需要訂閱的資料來源,並將其傳入 Subscription 生成一個 subscription 物件
                const parentSub = (this.propsMode ? this.props : this.context)[subscriptionKey]
                this.subscription = new Subscription(this.store, parentSub, this.onStateChange.bind(this))

                this.notifyNestedSubs = this.subscription.notifyNestedSubs.bind(this.subscription)
            }
            // 資料的監聽函式
            onStateChange() {
                this.selector.run(this.props)
            }
            
            // 將 selector.props 傳入到傳入的元件
            render() {
                const selector = this.selector
                selector.shouldComponentUpdate = false
                return createElement(WrappedComponent, selector.props)
            }

        }
        
        // 上面定義的 type 將作為 Connect 元件的屬性
        Connect.WrappedComponent = WrappedComponent
        Connect.displayName = displayName
        Connect.childContextTypes = childContextTypes
        Connect.contextTypes = contextTypes
        Connect.propTypes = contextTypes
        // 將傳入的元件的屬性複製進父元件
        return hoistStatics(Connect, WrappedComponent)
    }
}
複製程式碼

實用進階

動態載入 reducer

場景:

  1. 隨著專案的增大與業務邏輯越來越複雜,資料狀態與業務元件(WrappedComponent)也會越來越多,在初始化 store 的時候將所有reducer注入的話會使得資源很大
  2. 在入口將所有 reducer 注入的話,由於業務比較多,所以不一定都能用到,造成資源與效能浪費

方案:

利用 redux.combineReducersstore.replaceReducer 組合與更新reducer

// 初始化 store 的時候,將 reducer 記錄下來
// initReducer: 初始化時的 reducer 物件
var reducers = combineReducers(initReducer);
const store = createStore(
    reducers,
    initState
);
store.reducers = initReducer;

// 載入子元件的時候,動態將新的 reducer 注入
function assignReducer(reducer) {
    // 合併新老 reducer
    const newReducer = Object.assign(store.reducers, reducer);
    // 經 combineReducers 組合後進行替換
    store.replaceReducer(combineReducers(newReducer));
}
複製程式碼

時間旅行

場景:

  1. redux 是一個狀態管理器,我們的對 action 以及 reducer 的操作,其實最終的目的就是為了更新狀態,而時間旅行就是記錄我們的狀態更新的軌跡,並能回到某一個軌跡的節點。
  2. 比較容易理解的一個場景比如說翻頁,記錄每頁的資料,頁碼變化的時候直接恢復資料不用再次請求介面(並不適合實際業務場景)
  3. 時間旅行更大的作用其實在於我們可以監控狀態的變化,更方便的除錯程式碼

方案:

利用 store.subscribe, 監聽 dispatch 時記錄下此時的 狀態

const stateTimeline = [ initState ];  // 記錄狀態的時間線
let stateIndex = 0;   // 當前所處狀態的索引

// 當時間節點發生改變的時候,更替 state
const reducer = (state, action) => {
    switch (action.type) {
        case 'CHANGE_STATE_INDEX':
            const currentState = action.playload.currentState;
            return currentState;
        default:
            return state;
    }
};

const saveState = () => {
    // 將當前狀態push進時間線
    stateTimeline.push(store.getState); 
    stateIndex++;
};

// 註冊監聽事件
store.subscribe(saveState);

// 獲取某個時間節點的 state
const getSomeNodeState = () => {
    return stateTimeline[stateIndex];
};


// 時間線控制器
const timeNodeChangeHandle = (someIndex) => {
    stateIndex = someIndex;
    store.dispatch({
        type: 'CHANGE_STATE_INDEX',
        playload: {
            currentState: getSomeNodeState();
        }
    });
};

複製程式碼

相關文章