redux 時間旅行,你值得擁有!

wuyafeiJS發表於2019-03-02

啥叫時間旅行?

顧名思義,就是可以隨時穿越到以前和未來,讓應用程式切換到任意時間的狀態。我們都知道,一般應用狀態都很複雜,建立、維護、修改和弄明白有哪些行為會影響狀態都不是一件容易的事兒。

redux 的解決方案

整個應用的 state 被儲存在一棵 object tree 中,並且這個 object tree 只存在於唯一一個 store 中。並使用純函式計算下一個應用程式狀態(不允許其他途徑對 state 進行修改)。這些特徵使 Redux 成為了一個可預測 的狀態容器,這意味著如果給定一個特定應用程式狀態和一個特定操作,那麼應用程式的下一個狀態將始終完全相同。這種可預測性使得實現時間旅行變得很容易。redux 也相應的開發了一個帶時間旅行的開發者工具redux-devtools

redux 時間旅行,你值得擁有!

就是上面這個東西。下面就讓我們跟隨例子一起來了解下 redux 時間旅行的工作原理。

閱讀要求

  • react 基礎
  • redux 基礎,明白 action,reducer,state 的關係。明白 combineReducer 的原理。

開始

專案地址:(github)[github.com/wuyafeiJS/r…]

預覽:

redux 時間旅行,你值得擁有!

既然我們要實現時間旅行,那麼第一步我們需要一個物件來記錄每一次狀態:stateHistory.js

export default {
  past: [],
  futrue: [],
  present: undefined,
  gotoState(i) {
    const index = i * 1;
    const allState = [...this.past, this.present, ...this.futrue];
    this.present = allState[index];
    this.past = allState.slice(0, index);
    this.futrue = allState.slice(index + 1, allState.length);
  }
};
複製程式碼

我們把狀態分為三個時間段:過去,現在(只有一個狀態),將來。gotoState 函式則是用來做時間旅行的,他的實現方式就是整合所有狀態 allState,重新分配,present 前面是 past,後面是 future。

那麼我們如何去存放每一次變更的狀態呢?我們需要找到一個入口,這個入口必須是每次觸發狀態變更都會經過的地方。而觸發狀態變更唯一的方式就是dispatch(action),想想,這樣的地方好像只有一個地方,看過 redux 原始碼的同學肯定就是不陌生,那就是 combineReducer 生成的 reducers 純函式。
combineReducer 負責整合多個 reducer,最終返回一個能夠處理所有 action 的 reducers。讓我們大致簡單實現一下:

const combineReducer = obj => (state, action) => {
  const finalState = {};
  for (key in obj) {
    finanlState[key] = obj[key](state[key], action);
  }
  return finalState; // 全域性state
};
複製程式碼

接下來,讓我們利用函數語言程式設計的思想加強下 reducers 的功能,讓它能記錄 state:reducers.js

import stateHistory from `./stateHistory`;// 引入我們之前宣告的history物件

// 原本我們是這樣返回reducers的
export default combineReducers({
    books: fetchReducer,
    displayMode: bookDisplayReducer,
    currentStatus: statusReducer,
    topic: topicReducer
})
// 改造後如下:
export default history(
  combineReducers({
    books: fetchReducer,
    displayMode: bookDisplayReducer,
    currentStatus: statusReducer,
    topic: topicReducer
  })
);
// 我們用history包裹combineReducer,history實現如下
const history = reducers => (state, aciton) => {
  switch (action.type) {
    case `UNDO`: // 後退
      stateHistory.undo();
      break;
    case `REDO`: // 前進
      stateHistory.redo();
      break;
    case `GOTO`: // 定點指向
      stateHistory.gotoState(action.stateIndex);
      break;
    default:
      const newState = reducer(state, action);
      stateHistory.push(newState);// 每次dipatch(action)都會像將狀態儲存到stateHistory
  }
  return stateHistory.present; // 返回當前狀態
}
複製程式碼

完善下stateHistory.js

export default {
  ...

  hasRecord(type) {// 查詢是否有過去或者將來的狀態
    return this[type].length > 0;
  },
  hasPresent() { // 查詢是否有現在的狀態
    return this.present !== undefined;
  },
  setPresent(state) {
    this.present = state;
  },
  movePresentToPast() {
    this.past.push(this.present);
  },
  push(currentState) { // 將狀態都儲存,並更新當前狀態
    if (this.hasPresent()) {
      this.past.push(this.present);
    }
    this.setPresent(currentState);
  },
  getIndex() { // 獲取當前狀態index
    return this.past.length;
  },
  undo() { // 後退
    if (this.hasRecord(`past`)) {
      this.gotoState(this.getIndex() - 1);
    }
  },
  redo() { // 前進
    if (this.hasRecord(`futrue`)) {
      this.gotoState(this.getIndex() + 1);
    }
  },
  ...
};
複製程式碼

配置 action:actions.js

...
export const redo = () => ({
  type: `REDO`
});

export const undo = () => ({
  type: `UNDO`
});

export const gotoState = stateIndex => ({
  type: `GOTO`,
  stateIndex
});
複製程式碼

準備工作都已經做完,接下來我們們直接在 react 元件內加上觸發程式碼即可components/History.js

const History = ({ past, futrue, present, redo, undo, gotoState }) => {
  const styles = {
    container: {
      marginLeft: `20px`,
      cursor: `pointer`
    },

    link: { textDecoration: `none` },
    input: { cursor: `pointer` }
  };
  const RightArrow = () => (
    // 前進
    <a href="#" style={styles.link} onClick={() => redo()}>
      &#8594;
    </a>
  );

  const LeftArrow = () => (
    // 後退
    <a href="#" style={styles.link} onClick={() => undo()}>
      &#8592;
    </a>
  );
  const max = () =>
    (past ? past.length : 0) +
    (present ? 1 : 0) +
    (futrue ? futrue.length : 0) -
    1;
  const value = () => (past ? past.length : 0);
  return (
    <span>
      <input
        type="range"
        min={0}
        max={max()}
        value={value()}
        onChange={e => {
          gotoState(e.target.value);
        }}
        style={styles.input}
      />
      {past && past.length > 0 ? <LeftArrow /> : null}
      {futrue && futrue.length > 0 ? <RightArrow /> : null}
    </span>
  );
};
複製程式碼

以上!希望對大家理解 redux 有所幫助。

相關文章