Undo/Redo 在 Vuex 中使用状态快照

Undo/Redo in Vuex using state snapshot

上下文

我构建了一个可用于以下步骤的撤消-重做插件。

使用的步骤(概述)

  1. 在每个动作上添加之前的状态快照。
  2. 使用以 previousState 作为参数的自定义 replaceState 方法撤消。
  3. 使用以 previousState 作为参数的自定义 replaceState 方法重做。

目前正在处理性能问题,因为我们要处理巨大的嵌套状态。

性能问题

由于上述问题,undo/redo 变得迟缓且无法使用。

我有点卡住了。我哪里出错了,或者是否有更好的方法来实现复杂状态应用程序 undo/redo?

代码

done:用于撤消和存储状态快照。

undone:用于重做和存储状态快照。

GlobalStore.js:所有模块组合在一起的全局商店。

export const store = new Vuex.Store({

     modules: { canvasEditor,dashboardMetrics }
     plugins: [ canvasEditorUndoRedo ]

})

以下是插件中的文件。

index.js

import { Operations } from ‘./Operations’;

import { actionsToBeUsed } from './constants';

import { cloneDeep } from 'lodash';

export const op = new Operations();

export const canvasEditorUndoRedo = (store) => {

  /*

        Description: Perform undo/redo operation
        Steps:
        1.Initalize the class
        2.Store the previous state based on actions

  */
  op.init(store);

  store.subscribeAction((action, state) => {
    if (action.type != 'undo' &&action.type != 'redo' && actionsToBeUsed.find(actionType => actionType == action.type) != undefined) {
        // Store the state
        let stateClone = cloneDeep(state.canvasEditor);
        op.addSnapshot({ id: op.done.length + 1, action, state: stateClone });
    }
  });
}

Operation.js

import { cloneDeep } from 'lodash';
export class Operations {

  store
  done=[]
  undone = []

  init(store) {
    this.store = store;
  }

  addSnapshot(snapshot) {
    this.done.push(snapshot);
    this.updateOperationsCount();
  }

  clearUndo() {
    this.done = [];
    this.updateOperationsCount();
  }

  clearRedo() {
    this.undone = [];
    this.updateOperationsCount();
  }

  undo() {

    /*
    Description: Performs undo operation based on previousState stored in array(done).
    Steps:

        I.    Get the last stored action from array(done)  pop it out.
        II.  Push the undo element(popped element) to redo’s an array (undone).
        III.   Replace the current state with stored state.

  */

    if (this.done.length != 0) {

      //I
      let undoELement = this.done.pop();

      //II
      this.undone.push({ id: undoELement.id, action: undoELement.action, state: this.store.state.canvasEditor});


      //III
      let state = cloneDeep(undoELement.state);
      this.replaceCanvasEditorState(state); 
      this.updateOperationsCount();

  }

  redo() {

    /*

    Description: Performs redo operation based on State stored in array(undone).
    Steps:
       I.   Get (pop) the last undo element from undone
      II.   Push the undo element(popped element) to undo’s an array (done) .
      III.   Replace the current state with stored state.
  */

    if (this.undone.length != 0) {

      //I
      let redoELement = this.undone.pop();

      //II
      this.done.push({ id: redoELement.id, action: redoELement.action, state: this.store.state.canvasEditor });

      //III
      let state = cloneDeep(redoELement.state);
      this.replaceCanvasEditorState(state);
      this.updateOperationsCount();
    }
  }

  replaceCanvasEditorState(state) {
  /*
     Description: Replaces current state with state provided as parameter.
  */
    this.store._withCommit(() => {
      this.store.state.canvasEditor = state;
    });

  }

  updateOperationsCount() {

    let undoRedo = {
      doneLength: this.done.length,
      undoneLength: this.undone.length
    }
    this.store.commit('updateUndoRedoCount', undoRedo);

  }

}

CodeSandBox

该应用程序处理很多复杂的操作。

恐怕使用快照(深度克隆)在巨大的嵌套状态上实现 undo/redo 总是很慢,更不用说内存占用了

所以你最好的选择可能是完全改变你的策略。当您使用 Vuex 时,每个更改都必须使用突变来完成,因此使用 action/reverse 操作策略来实现它应该不会太难。在每个突变中,不是将状态快照推送到堆栈上,而是将“反向操作”突变推送到堆栈上(使用旧的 sub-state 作为参数 - deepCloned 如果它是一个对象)。

当然它不像您原来的方法那样透明,并且需要突变作者以特定方式编写突变,但您总是可以编写一些帮助程序以使其更容易和“标准化”...

或者您可以查看 undo-redo-vuex Vuex 插件,它通过保存初始状态和执行的所有变更来实现 undo/redo。当需要撤消时,它将状态重置为初始状态并重播除最后一个之外的所有突变。不知道这种扩展有多好,但至少它对突变作者来说更透明...