时间旅行
时间旅行就是可以随时穿越到过去或未来,让应用程序可以在自己的历史状态里面任意穿梭。我们日常工作中的许多软件都有时间旅行的功能,例如 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 命令,结果如下: