javascript编写_介绍用javascript编写撤消重做系统

javascript编写

When designing applications focused on the creation or modification of data, like text or image editors, for example, a common desire for your end-user is the ability to undo or redo their actions. It’s an important consideration, because the knowledge that steps can be safely and easily undone gives your users confidence in using your app.

例如,在设计专注于数据创建或修改的应用程序时,例如文本或图像编辑器,最终用户的共同愿望是能够撤消或重做其操作。 这是一个重要的考虑因素,因为可以安全,轻松地撤消步骤的知识使您的用户对使用您的应用程序充满信心。

So, you’ve decided to try integrating an undo system into your project, but you’ve never written anything like that before. How do they work? Where do you even start? This article aims to give you a little direction by introducing you to how undo systems work and how to write one.

因此,您决定尝试将撤消系统集成到您的项目中,但是您之前从未写过类似的东西。 它们如何工作? 你什至从哪里开始? 本文旨在通过向您介绍撤消系统的工作方式和编写方式,为您提供一些指导。

一个“不可行的”柜台 (An “Undoable” Counter)

Let’s start this off with a simple (and extremely common) example: a counter!

让我们以一个简单(且极为常见)的示例开始:一个计数器!

Mind-blowing, right? This code defines a function for generating counters that can be incremented or decremented using the increment and decrement methods! …yeah, there’s not much to it. And its behaviour is similarly predictable:

令人振奋吧? 该代码定义了一个生成计数器的函数,该计数器可以使用incrementdecrement方法递增或递​​减! …是的,没有太多。 其行为类似地是可预测的:

While nothing to write home about, this code provides the perfect place to begin exploring undo operations. It’s easy to understand what it’s doing, and it has state data that is subject to change. Let’s see if we can add an undo function to it!

尽管没有什么值得写的,但是这段代码为开始研究撤消操作提供了理想的场所。 很容易理解它在做什么,并且它的状态数据可能会发生变化。 让我们看看是否可以向其添加撤消功能!

The easiest way we could implement undo operations would be to keep track of the counter’s value whenever it changes, and add it to a “history” array that we can use to revisit previous states. We’ll also keep a pointer to the specific index of the array that represents the present value of the counter in a variable called “position”. With both a history array and a position pointer, we no longer need to keep a separate value variable, and will replace it with a function that returns the element of history at the current position.

我们执行撤消操作的最简单方法是随时跟踪计数器的value变化,并将其添加到“历史”数组中,以用于重新访问以前的状态。 我们还将在指向“ position”的变量中保留一个指向数组特定索引的指针,该索引代表计数器的当前值。 使用历史记录数组和位置指针,我们不再需要保留单独的value变量,而将其替换为返回当前position处的history元素的函数。

Image for post
Illustration of a State History based Undo system
基于状态历史记录的撤消系统的图示

The implementation might look like this:

该实现可能如下所示:

With this in place, all we need to do to enable undo and redo for our state changes is decrementing and incrementing the history position! Knowing this, we can go ahead and add the undo and redo methods:

完成此操作后,为状态更改启用撤消和重做操作所需要做的就是减少和增加历史记录位置! 知道了这一点,我们可以继续添加undo和redo方法:

Simple, right? All that’s left is to modify the increment and decrement methods to actually push new values to history and update the position accordingly. We’ll add a new method, setValue, to assist with that:

简单吧? 剩下的就是修改incrementdecrement方法,以将新值实际推入history并相应地更新position 。 我们将添加一个新方法setValue ,以协助完成该工作:

The setValue function accepts a new value, removes any states ahead of the current position in history (making a new change after a series of undos should clear any existing ‘future’ states), pushes the new value to history, and points position to the new state. The increment and decrement methods now use it, as well as the new value method, to perform the same tasks they did before.

setValue函数接受一个新值,删除历史记录中当前位置之前的所有状态(在一系列撤消操作后应清除所有现有的“未来”状态,然后进行新更改),将新值推入history ,并将position指向新状态。 现在, incrementdecrement方法以及新value方法都使用它来执行以前执行的相同任务。

All in all, our new “undoable” counter should look like this:

总而言之,我们新的“不可撤销”计数器应如下所示:

And we can try it out with some simple driver code:

我们可以使用一些简单的驱动程序代码进行尝试:

Success! Our new object is capable of remembering its past states and revisiting them with its undo and redo methods!

成功! 我们的新对象能够记住其过去状态,并通过其undoredo方法重新访问它们!

更健壮的方法 (A More Robust Approach)

The previous approach is a good introduction to history traversal, and works well enough for tracking a single, simple variable. However, trying to track more complex data exposes some shortcomings. What if you wanted to undo changes made to an object this way? Would you deep copy the entire object into the history at every change? If the object contains a significant amount of data, making many small changes to it could quickly start to eat up all your available memory!

先前的方法很好地介绍了历史遍历,并且足以跟踪单个简单变量。 但是,尝试跟踪更复杂的数据会暴露出一些缺点。 如果您想以这种方式撤消对对象所做的更改怎么办? 您是否会在每次更改时将整个对象深度复制到历史记录中? 如果对象包含大量数据,则对其进行许多小的更改可能会很快开始耗尽所有可用内存!

To avoid that, we’ll turn to a common programming design pattern: the Command pattern. Explaining the pattern in-depth is beyond the scope of this article, but for our purposes you can think of it as taking the actions you wish to perform on your data and turning them into objects that can be passed around, stored, and executed at any time by a controller.

为了避免这种情况,我们将转向一种常见的编程设计模式: Command模式。 深入解释模式不在本文讨论范围之内,但是出于我们的目的,您可以将其视为对数据执行希望执行的操作并将其转换为可以在其中传递,存储和执行的对象。随时由控制器执行。

Abstracting the actions into command objects allows us to store a history of all the changes we’ve made to an object, rather than a history of that object’s states.

将动作抽象为命令对象使我们可以存储对对象所做的所有更改的历史记录,而不是该对象状态的历史记录。

Image for post
Illustration of a Command History based Undo system
基于命令历史记录的撤消系统的图示

Let’s take this new concept and make it familiar by applying it to a similar situation as our last example.

让我们采用这个新概念,并通过将其应用于与上一个示例类似的情况来使其熟悉。

So in this case we have yet another counter. This time, however, its state is an object with multiple properties: a name and a count. It also doesn’t have any built-in increment or decrement methods. We’ll be writing commands to take on that responsibility!

因此,在这种情况下,我们还有另一个柜台。 但是这一次,它的状态是一个具有多个属性的对象: namecount 。 它也没有任何内置的增量或减量方法。 我们将编写命令来承担这一责任!

In the case of an undo/redo system, a command should be an object that at the very least contains two methods: execute and undo. As the names imply, execute performs an action on the data, and undo restores it to its previous state.

对于撤消/重做系统,命令应该是至少包含两个方法的对象: executeundo 。 顾名思义, execute对数据执行操作,然后undo将其还原到以前的状态。

Let’s see what increment might look like when implemented as a command:

让我们看看当作为命令实现时, increment可能是什么样的:

When a new increment command is created, it takes in the counter object it is modifying as a parameter, and stores the current value of the counter in a variable called previousCount. Its execute method increases the value of the counter’s count by one, and its undo method restores its value to the state stored in previousCount.

创建新的递增命令时,它将接受正在修改的计数器对象作为参数,并将计数器的当前值存储在一个名为previousCount的变量中。 它的execute方法将计数器的count增加一,其undo方法将其值恢复到存储在previousCount的状态。

Given the simplicity of incrementing a counter, the undo method could also be implemented as simply decrementing it. However, storing the previous state of any changed variables is a more flexible solution that can apply to more complex operations, so I’ve elected to demonstrate that technique here.

鉴于增加计数器的简单性,撤消方法也可以实现为简单地将其递减。 但是,存储任何已更改变​​量的先前状态是一种更灵活的解决方案,可以应用于更复杂的操作,因此,我选择在此处演示该技术。

Decrementing is essentially the same but with subtraction instead of addition in its execute method:

减量本质上是相同的,但是在其execute方法中用减法代替加法:

But this is only half of the story. If we’re making commands, we also have to have a way to store them, apply them, and manage a command history to enable a chain of undos and redos! Let’s define a command manager to take care of that for us.

但这只是故事的一半。 如果要执行命令,还必须有一种方法来存储它们,应用它们并管理命令历史记录,以实现一连串的撤消和重做! 让我们定义一个命令管理器来为我们解决这个问题。

Whoa! That’s a bunch of new stuff! Some of it should look familiar, though. The managing of history and position, as well as the history traversal in the undo and redo methods, are pretty much the same as what we did in the last example. So we’ll focus instead on what’s new here.

哇! 那是一堆新东西! 不过,其中一些看起来应该很熟悉。 historyposition的管理以及undoredo方法中的历史遍历与我们在上一个示例中所做的几乎相同。 因此,我们将重点放在此处的新功能上。

INCREMENT, DECREMENT, and commands are all constants used to ease the process of selecting a particular command to generate. The string constants prevent string input errors, and the commands object exists to take the place of a switch statement, allowing you to access a particular command generator function by its associated string. You can learn more about using objects in place of switches here, if you like.

INCREMENTDECREMENTcommands都是用于简化选择要生成的特定命令的过程的常量。 字符串常量可防止字符串输入错误,并且存在commands对象来代替switch语句,从而使您可以通过与其关联的字符串访问特定的命令生成器函数。 如果愿意,您可以在此处了解更多有关使用对象代替开关的信息

createCommandManager takes an object as a parameter in its creation to use as a target to apply its commands to (not always necessary for the pattern, but it fits well with the rest of the implementation here). It returns an object with three methods: doCommand, undo, and redo.

createCommandManager在创建对象时将对象作为参数,以用作对其应用命令的目标(模式并非总是必需的,但与此处的其余实现非常吻合)。 它返回具有三个方法的对象: doCommandundoredo

doCommand is where the magic happens. First, it clears any future commands left by undos, much like setValue does in our last example. It then checks to see if the command string it was passed exists in the commands object, and if so, it creates a new command object from it, targetting target. It pushes the new command to the history array, then executes it, thereby applying its changes to target.

doCommand是神奇的地方。 首先,它清除撤消操作留下的所有将来命令,就像在上一个示例中setValue所做的一样。 然后,它检查所传递的命令字符串是否存在于commands对象中,如果存在,则从中创建一个新的命令对象,目标为target 。 它将新命令推送到历史记录数组,然后执行它,从而将其更改应用于target

As mentioned earlier, undo and redo are very similar to the previous example, but now that we’re dealing with a history of commands, undo runs the command’s undo (and redo runs execute) to update the object’s state.

如前所述, undoredo与前面的示例非常相似,但是现在我们正在处理命令的历史记录, undo运行命令的undo ( redo运行execute )以更新对象的状态。

Now that we’ve got all the pieces of the puzzle together, let’s try out some driver code and see it all in action!

现在,我们已经把所有的难题都解决了,让我们尝试一些驱动程序代码,看看它们在起作用!

Yes! It works as expected! We’re able to increment and decrement our counter and undo changes to it via the command manager, all without touching or having to keep track of the name variable!

是! 它按预期工作! 我们可以通过命令管理器来增加和减少计数器,并撤消对计数器的更改,而无需触摸或跟踪名称变量!

If you’ve been following along, feel free to pat yourself on the back, because you’ve learned how to time travel! At least in the context of your application state. :)

如果您一直在追随,请随时向后轻拍一下,因为您已经学会了如何定时旅行! 至少在您的应用程序状态的上下文中。 :)

If you want to learn more about the command pattern and undo systems, feel free to check out these resources I used in writing this article:

如果您想了解有关命令模式和撤消系统的更多信息,请随时查看我在撰写本文时使用的这些资源:

翻译自: https://medium.com/code-education/intro-to-writing-undo-redo-systems-in-javascript-af17148a852b

javascript编写

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值