如何制作撤销/重做功能

25

我想在我的脚本中添加一个“撤销/重做”功能。我查看了一些建议,大多数建议使用“命令模式”。

该功能必须适用于一个页面 - 在页面重新加载后,该功能必须能够“重做/撤销”最后的操作。

我不知道命令模式是如何工作的,我考虑创建一个对象,来存储函数的名称、旧值和新值,但我不确定这是否是有效的方法。

也许有人可以给我一个小的示例,展示一个“撤销/重做”函数的代码应该是什么样的。

1个回答

63

有两种常见实现撤销/重做的方法:

此方法易于实现,但效率低下,因为您需要存储相似副本的整个状态。

  • 命令模式,其中您捕获影响状态的命令/操作,当前动作及其反向操作。

这种方式更难实现,因为对于您应用程序中的每个可撤销操作都必须明确编写其反向操作,但它更节省内存,因为您只存储影响状态的操作。

现在让我们详细介绍这两种模式:

备忘录模式

在应用程序执行操作之前,您会对当前状态进行快照,并将其保存到数组中。该快照称为备忘录

如果用户想要撤消,则只需pop上一个备忘录并应用它即可。程序将返回到在应用最后一个动作之前的状态。

这种模式占用内存较多;每个备忘录相对较大,因为它捕获了整个当前状态。

但它也是最容易实现的,因为您不需要像命令模式(见下文)中那样显式编写所有情况及其反向操作。

const mementos = []
const input = document.querySelector('input')

function saveMemento() {
  mementos.push(input.value)
}

function undo() {
  const lastMemento = mementos.pop()
   
  input.value = lastMemento ? lastMemento : input.value
}
<h4> Type some characters and hit Undo </h4>
<input onkeydown="saveMemento()" value="Hello World"/>
<button onclick="undo()">Undo</button>

命令模式

对于用户采取的每个操作,您都需要保存相应的逆操作/命令。例如,每次在文本框中添加字符时,都要保存逆函数;即删除该位置处的字符。

如果用户希望撤消,则弹出最后一个逆操作/命令并应用它。

const commands = []
const input = document.querySelector('input')

function saveCommand(e) {
  commands.push({
    // the action is also saved for implementing redo, which
    // is not implemented in this example.
    action: { type: 'add', key: e.key, index: input.selectionStart },
    inverse: { type: 'remove', index: input.selectionStart }
  })
}

function undo() {
  let value = input.value.split('')
  const lastCommand = commands.pop()
 
  if (!lastCommand) return
    
  switch (lastCommand.inverse.type) {
    case 'remove':
      value.splice(lastCommand.inverse.index, 1)
      break;      
  }
  
  input.value = value.join('')
}
<h4> Type some characters and hit Undo </h4>
<input onkeydown="saveCommand(event)" value="Hello World"/>
<button onclick="undo()">Undo</button>

我写的这些片段仅在添加字符时才有效,然后撤消以返回到添加字符之前的状态,因此它们是对您应该如何实现此操作的过度简化。

尽管如此,我认为它们演示了两种模式的核心概念。

顺便说一句,我在我的项目中使用UndoManager作为我的命令堆栈。


就像你所写的,我认为命令模式是更好的选择。能否为我编写一个小脚本,以便查看、解释和理解在JS代码中应该如何实现? - RainerS
8
根据应用程序的类型,可能更适合使用“命令备忘录”。例如,在图形工具中,您不总是能够有一个反向命令,并且保存每个完整状态会在短时间内耗尽内存。因此,存储所有命令并按顺序调用它们以进行撤消是正确的方法。 - Kaiido
3
@Kaiido,我之前不知道这个。基本上你是重复执行所有指令直到达到当前状态的前一个状态,对吗? - nicholaswmin
2
是的,就是这样。 - Kaiido
1
@BenButterworth 这是一个不错的想法,但如果某些命令需要很长时间才能完成(比如应用Photoshop滤镜等),可能会遇到性能问题。在这种情况下,定期保存整个项目的快照可能是明智的选择,特别是在长时间运行的命令之后,这样你就可以有另一个起点,避免等待长时间命令的完成。 - undefined
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接