啥叫時間旅行?
顧名思義,就是可以隨時穿越到以前和未來,讓應用程式切換到任意時間的狀態。我們都知道,一般應用狀態都很複雜,建立、維護、修改和弄明白有哪些行為會影響狀態都不是一件容易的事兒。
redux 的解決方案
整個應用的 state 被儲存在一棵 object tree 中,並且這個 object tree 只存在於唯一一個 store 中。並使用純函式計算下一個應用程式狀態(不允許其他途徑對 state 進行修改)。這些特徵使 Redux 成為了一個可預測 的狀態容器,這意味著如果給定一個特定應用程式狀態和一個特定操作,那麼應用程式的下一個狀態將始終完全相同。這種可預測性使得實現時間旅行變得很容易。redux 也相應的開發了一個帶時間旅行的開發者工具redux-devtools
就是上面這個東西。下面就讓我們跟隨例子一起來了解下 redux 時間旅行的工作原理。
閱讀要求
- react 基礎
- redux 基礎,明白 action,reducer,state 的關係。明白 combineReducer 的原理。
開始
專案地址:(github)[github.com/wuyafeiJS/r…]
預覽:
既然我們要實現時間旅行,那麼第一步我們需要一個物件來記錄每一次狀態: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()}>
→
</a>
);
const LeftArrow = () => (
// 後退
<a href="#" style={styles.link} onClick={() => undo()}>
←
</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 有所幫助。