时间旅行
时间旅行就是可以随时穿越到过去或未来,让应用程序可以在自己的历史状态里面任意穿梭。我们日常工作中的许多软件都有时间旅行的功能,例如 Office 和 Photoshop 的 「撤销」和「重做」的功能。
备忘录模式
所谓备忘录模式就是在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样可以在以后将对象恢复到原先保存的状态。而时间旅行就是设计模式中的备忘录模式,它保存了应用程序的历史状态,以便应用程序可以恢复到某个时刻的状态。
Redux 时间旅行
Redux 使用对象来表示状态,并使用纯函数计算下一个应用程序状态。这些特征使 Redux 成为了一个 可预测 的状态容器,这意味着如果给定一个特定应用程序状态和一个特定操作,那么应用程序的下一个状态将始终完全相同。这种可预测性使得实现时间旅行变得很容易 — 能够在应用程序以前的状态中前后移动,并实时查看结果。redux 也相应的开发了一个带时间旅行的开发者工具redux-devtools
如上图,拖动滑动条,即可以回退或前进到某个状态
功能实现
我们使用一个对象来记录每一次状态,并把状态分为三个时间段:
- 过去(过去状态数组)
- 现在(只有一个状态)
- 将来(将来状态数组)
gotoState 函数则是用来做时间旅行的,它把过去、现在、将来的状态整合后重新分配。
module.exports = createHistory = () => {
// timeline 对象记录所有的状态
const timeline = {};
// 过去状态
timeline.past = [];
// 当前状态
timeline.present = undefined;
// 将来状态
timeline.future = [];
// 整合所有状态,重新分配
timeline.gotoState = (index) => {}
}
gotoState 方法实现
gotoState 方法整合所有的状态,然后根据 index 重新分配过去、现在、将来的状态
timeline.gotoState = (index) => {
const allState = [...timeline.past, timeline.present, ...timeline.future];
timeline.present = allState[index];
timeline.past = allState.slice(0, index);
timeline.future = allState.slice(index + 1, allState.length);
// 其他方法实现
return timeline;
}
getIndex 方法实现
getIndex 方法获取当前状态(即现在状态)的位置
timeline.getIndex = () => {
return timeline.past.length
}
push 方法实现
push 方法保存当前状态
timeline.push = (currentState) => {
if (timeline.present) {
// 将之前的 present 状态保存到 过去状态中,将之前的当前状态变成过去状态
timeline.past.push(timeline.present);
}
// 更新当前状态
timeline.present = currentState;
}
undo 方法实现
undo 方法是回退到上一个状态
// 后退
timeline.undo = () => {
if (timeline.past.length !== 0) {
// 当前状态的位置 减 1 就是上一个状态的位置
timeline.gotoState(timeline.getIndex() - 1)
}
}
redo 方法实现
redo 方法是前进一个状态
timeline.redo = () => {
if (timeline.future.length !== 0) {
// 当前状态的位置 加 1 就是下一个状态的位置
timeline.gotoState(timeline.getIndex() + 1);
}
}
完整代码
module.exports = createHistory = () => {
const timeline = {};
// 过去状态
timeline.past = [];
// 现在状态
timeline.present = undefined;
// 将来状态
timeline.future = [];
// 整合所有的状态,然后根据 index 重新分配过去、现在、将来的状态
timeline.gotoState = (index) => {
const allState = [...timeline.past, timeline.present, ...timeline.future];
timeline.present = allState[index];
timeline.past = allState.slice(0, index);
timeline.future = allState.slice(index + 1, allState.length);
}
// 获取当前状态的位置
timeline.getIndex = () => {
return timeline.past.length;
}
// 保存当前状态
timeline.push = (currentState) => {
// 将之前的 present 状态保存到 过去状态中,将之前的当前状态变成过去状态
if (timeline.present) {
timeline.past.push(timeline.present);
}
// 更新当前状态
timeline.present = currentState;
}
// 回退到上一个状态
timeline.undo = () => {
if (timeline.past.length !== 0) {
timeline.gotoState(timeline.getIndex() - 1);
}
}
//前进下一个状态
timeline.redo = () => {
if (timeline.future.length !== 0) {
timeline.gotoState(timeline.getIndex() + 1);
}
}
return timeline;
}
测试用例
undo
it("撤销undo ", () => {
const history = createHistory()
history.push({num: 1})
history.push({num: 2})
history.push({num: 3})
history.undo()
expect(history.present.num).toBe(2)
});
redo
it("恢复redo ", () => {
const history = createHistory()
history.push({num: 1})
history.push({num: 2})
history.push({num: 3})
history.push({num: 4})
history.undo()
history.undo()
history.undo()
history.redo()
expect(history.present.num).toBe(2)
});
定点漂移
it("定点回退 ", () => {
const history = createHistory()
history.push({num: 1})
history.push({num: 2})
history.push({num: 3})
history.gotoState(1)
expect(history.present.num).toBe(2)
});
测试结果
执行 jest time-travel --watchAll 命令,结果如下: