前端手写方法系列之时间旅行

692 阅读3分钟

时间旅行

时间旅行就是可以随时穿越到过去或未来,让应用程序可以在自己的历史状态里面任意穿梭。我们日常工作中的许多软件都有时间旅行的功能,例如 Office 和 Photoshop 的 「撤销」和「重做」的功能。

备忘录模式

所谓备忘录模式就是在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样可以在以后将对象恢复到原先保存的状态。而时间旅行就是设计模式中的备忘录模式,它保存了应用程序的历史状态,以便应用程序可以恢复到某个时刻的状态。

Redux 时间旅行

Redux 使用对象来表示状态,并使用纯函数计算下一个应用程序状态。这些特征使 Redux 成为了一个 可预测 的状态容器,这意味着如果给定一个特定应用程序状态和一个特定操作,那么应用程序的下一个状态将始终完全相同。这种可预测性使得实现时间旅行变得很容易 — 能够在应用程序以前的状态中前后移动,并实时查看结果。redux 也相应的开发了一个带时间旅行的开发者工具redux-devtools

如上图,拖动滑动条,即可以回退或前进到某个状态

功能实现

我们使用一个对象来记录每一次状态,并把状态分为三个时间段:

  1. 过去(过去状态数组)
  2. 现在(只有一个状态)
  3. 将来(将来状态数组)

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 命令,结果如下: