关于在web端回撤重做的实践

355 阅读3分钟

背景

由于最近工作中在web端要实现一个富应用,其中很多操作都是会改变状态的操作,所以当我们想实现一个回撤重做的的时候,并没有设计模式那般简单,以此记录下来,本人水平有限,欢迎指正。

业界实现方案

前提:我们需要明确三个概念:

  • 操作都是针对一个封闭区域范围内的数据,即状态
  • 操作分有副作用操作跟无副作用操作,即会改变状态跟不改变状态
  • 状态的变更,概括一下无非增删改

实际方案:

  • 命令式:将每一个操作都抽象为行为,独立封装成函数,并且每一个行为都具有与之对应的反行为。执行行为,即执行该操作,状态被修改到下一个状态,执行反行为,状态被修改到上一个状态。
  • 状态式:不关注行为或操作,只关心状态,将每一次操作的状态记录下来,回撤重做时,将状态置为上一次的状态即可。
  • 栈顺序执行式:将每一个操作抽象为行为,不关心反行为,将每一步行为记录下来,想要回撤或重做时,从头执行到想要返回的状态那一步。

优缺点比对:

  • 命令式无疑在时间复杂度,空间复杂度上是最优选择,但无疑,行为的抽象,以及反行为的编写最为复杂,在数据状态占据大空间时适用
  • 状态式编写容易,在一些数据状态占用空间较少的场景下比较适用,如文本编辑,但数据状态占据大空间时,一次操作就会让存储空间增加一倍,如500M的状态,三次操作直接1.5G爆栈。
  • 栈顺序执行,编写容易,适用于数据状态占用小空间场景,对于大空间场景,如果每一步操作对状态的增删改的变动较大,容易出现时间复杂度较高,页面假死的现象,无法进行任何操作。

综上,结合我们的场景需求(大空间,希望用户能够经可能的多次操作),我选择了命令式,毕竟,我们希望不要去限制用户的行为。

在编写中遇到的技巧点:

  • 区分哪些操作是有副作用,哪些是无副作用,只需关注会改变状态的操作,并细分操作对状态的变化是否具有规律性。
  • 有的操作是直接覆盖了原有的部分状态,且无任何规律,那么如何编写反行为?针对这种毫无规律可言的行为,我们可以保存被覆盖前的部分状态,以小量的空间换取时间。
  • 有的操作有规律,如将某个局部状态数据放大,缩小,位置平移,反行为无非是针对局部数据的缩小,放大,反方向移动。
  • 尽可能的拆解操作,将操作归类到增删改里。

代码逻辑展示:

// 首先我们抽象出两个栈,用以存储行为
const redoStack = []
let undoStack = []

// 在操作触发行为的同时,生成与之对应的反行为,将其入redoStack
// 执行某一操作后===>
undoStack = [] // 执行操作后,清空undoStack,此时不可执行重做
const behavior = [
  {
    describ: '行为',
    params: xxx, //与之对应操作,向callFun传递的参数
    callFun: xxx , // 与之对应的行为方法
  },
  {
    describ: '反行为',
    params: xxx, //与之对应操作,向callFun传递的参数
    callFun: xxx , // 与之对应的行为方法
    sourceData: xxx, //如果行为无规律改变了源数据,则需要
  }
]
redoStack.push(behavior)

// 回撤
const popBehavior = redoStack.pop()
popBehavior[1].sourceData(popBehavior[1].params)
undoStack.push(popBehavior)

// 重做
const popBehavior = undoStack.pop()
popBehavior[0].sourceData(popBehavior[0].params)
redoStack.push(popBehavior)

总结

至此,通过双栈我们实现了该场景下回撤重做实践,其中栈只是一个辅助,双栈或单栈指针均可,谢谢大家阅读。