vue3的撤销与重做

前言

一个低代码项目遇到的撤销重做功能,基于vue3实现。

实现步骤

  1. 通过pinia来存储历史记录数组(arr)、目前页面所展示的对应的索引(index)、现在是否要生成快照(isSnapshot),作用是防止撤销重做的时候也会添加快照,因为撤销重做的时候会触发watch里面的方法、最大能够存储的数据数(maxStep)。在pinia定义几个方法。
  2. 第一个生成快照,watch来监听页面元素数组的变化,变化时调用方法,进入该方法后判断是否能生成快照,不可以将isSnapshot=true再退出,可以先判断一下index是否在队列的尾部,不在需要将index后面的元素删除掉,再将页面元素数组深复制push到历史记录数组里面。
	addSnapshot() {
          if (this.isSnapshot) {
              this.isFull();//判断是否溢出 溢出删除一个
              let n = this.snapshotData.length;
              // 当前索引不在开头需要恢复时
              if (this.curIndex < n-1) {
                  this.snapshotData.splice(this.curIndex+1);
              }
              // elStore().els为页面元素数组
              this.snapshotData.push(JSON.stringify(elStore().els));
              this.curIndex++;
          }
          this.isSnapshot = true;
      }
  1. 第二个判断是否溢出,溢出shift出一个元素。
        // 判断是否溢出
        isFull() {
            if (this.snapshotData.length == this.maxStep) {
                this.snapshotData.shift(0);
            }
        },
  1. 第三个撤销,如果index>0,证明现在可以撤销,然后把isSnapshot=false,再将index减1,后将index指向的元素数组渲染到页面。
		// undo撤销
        undo() {
            if (this.curIndex > 0) {
                this.isSnapshot = false;
                let t = JSON.parse(this.snapshotData[--this.curIndex]);
                for (let i = 0; i < t.length; i++) {
                    elStore().els[i] = t[i];
                }
            }
        },
  1. 第四个重做,如果没有到数组的最尾端证明可以重做,然后把isSnapshot=false,再将index加1,后将index指向的元素数组渲染到页面。
        // record 恢复
        record() {
            // 判断是不是到尾部,有才可以恢复 
            if (this.curIndex < this.snapshotData.length-1) {
                this.isSnapshot = false;
                let t = JSON.parse(this.snapshotData[++this.curIndex]);
                for (let i = 0; i < t.length; i++) {
                    elStore().els[i] = t[i];
                }
            }
        }

注意点

虽然简单方便但可能会引起数据的丢失。
缺点:

  1. 使用JSON.Stringify 转换的数据中,如果包含 function,undefined,Symbol,这几种类型,不可枚举属性,JSON.Stringify序列化后,这个键值对会消失。
  2. 转换的数据中包含 NaN,Infinity 值(含-Infinity),JSON序列化后的结果会是null。
  3. 转换的数据中包含Date对象,JSON.Stringify序列化之后,会变成字符串。
  4. 转换的数据包含RegExp 引用类型序列化之后会变成空对象。
  5. 无法序列化不可枚举属性。
  6. 无法序列化对象的循环引用,(例如: obj[key] = obj)。
  7. 无法序列化对象的原型链。

总体代码

// 监听事件,这里会遇到频繁触动的问题,用防抖解决
    watch(useElStore.els, debounce(() => {
      snapshotStore.addSnapshot();
    }, 400), { immediate: true });
    
 // 撤销
    function cancel() {
      changeRectShow();
      snapshotStore.undo();
    }

    // 重做
    function record() {
      changeRectShow();
      snapshotStore.record();
    }
// 防抖代码
export default (fn,delay) =>{
    let t = null;
    return () => {
        if (t != null) {
            clearInterval(t);
        }
        t = setTimeout(() => {
            fn();
        },delay)
    }
}
// snapshot仓库
import { defineStore } from 'pinia'
import elStore from './index'
export default defineStore('snapshot', {
    state() {
        return {
            snapshotData: [],// 快照数据
            maxStep: 30,// 最大能够存储的数据数
            curIndex: -1,// 当前所在下标
            isSnapshot: true,// 是否可以保存
        }
    },
    actions: {
        // 添加快照
        addSnapshot() {
            if (this.isSnapshot) {
                this.isFull();//判断是否溢出 溢出删除一个
                let n = this.snapshotData.length;
                // 当前索引不在尾部,说明进行了撤销,需要删除后面的元素
                if (this.curIndex < n-1) {
                    this.snapshotData.splice(this.curIndex+1);
                }
                this.snapshotData.push(JSON.stringify(elStore().els));
                this.curIndex++;
            }
            this.isSnapshot = true;
        },
        // 判断是否溢出
        isFull() {
            if (this.snapshotData.length == this.maxStep) {
                this.snapshotData.shift(0);
            }
        },
        // undo撤销
        undo() {
            if (this.curIndex > 0) {
                this.isSnapshot = false;
                let t = JSON.parse(this.snapshotData[--this.curIndex]);
                for (let i = 0; i < t.length; i++) {
                    elStore().els[i] = t[i];
                }
            }
        },
        // record 恢复
        record() {
            // 判断是不是到尾部,有才可以恢复 
            if (this.curIndex < this.snapshotData.length-1) {
                this.isSnapshot = false;
                let t = JSON.parse(this.snapshotData[++this.curIndex]);
                for (let i = 0; i < t.length; i++) {
                    elStore().els[i] = t[i];
                }
            }
        }
    }
})
  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Vue 中实现撤回和的功能,通常需要用到一个叫“命令模式”的设计模式。这个模式的基本思想是将操作封装成一个对象,以便能够在需要的时候执行或者撤销这个操作。 在具体实现中,我们可以定义一个 `Command` 类,表示一个操作,这个类中包含了一个 `execute` 方法和一个 `undo` 方法,分别表示执行和撤销这个操作。然后我们再定义一个 `CommandManager` 类,用来管理所有的操作,包括撤销。 在 Vue 中,我们可以通过 `watch` 监听数据的变化,然后在数据发生变化的时候,创建一个新的操作对象,并将其添加到 `CommandManager` 中。当需要撤销操作的时候,我们只需要从 `CommandManager` 中获取最近的一个操作对象,然后调用其 `undo` 方法即可。 需要注意的是,由于 `watch` 监听器会在数据发生变化时立即执行,因此我们需要在 `CommandManager` 中实现一个缓存机制,避免操作对象复添加到队列中。 以下是一个简单的代码示例: ```javascript class Command { constructor(data, oldValue) { this.data = data; this.oldValue = oldValue; } execute() { this.oldValue = this.data.value; this.data.value = this.data.newValue; } undo() { this.data.value = this.oldValue; } } class CommandManager { constructor() { this.commands = []; this.currentIndex = -1; } add(command) { // 如果当前操作不是最新的操作,说明需要清除之后的操作缓存 if (this.currentIndex < this.commands.length - 1) { this.commands.splice(this.currentIndex + 1); } this.commands.push(command); this.currentIndex++; } undo() { if (this.currentIndex >= 0) { const command = this.commands[this.currentIndex]; command.undo(); this.currentIndex--; } } redo() { if (this.currentIndex < this.commands.length - 1) { this.currentIndex++; const command = this.commands[this.currentIndex]; command.execute(); } } } const cm = new CommandManager(); new Vue({ el: '#app', data() { return { value: '', }; }, watch: { value(newValue, oldValue) { const command = new Command(this, oldValue); cm.add(command); }, }, methods: { undo() { cm.undo(); }, redo() { cm.redo(); }, }, }); ``` 在上面的代码中,我们创建了一个 `Command` 类表示一个操作对象,其中 `data` 表示操作的数据,`oldValue` 表示操作之前的值。`execute` 方法表示执行操作,`undo` 方法表示撤销操作。 另外,我们还创建了一个 `CommandManager` 类,用于管理所有的操作。其中 `commands` 数组表示所有的操作,`currentIndex` 表示当前执行的操作索引。`add` 方法表示添加一个新的操作,`undo` 方法表示撤销操作,`redo` 方法表示操作。 在 Vue 中,我们使用 `watch` 监听数据的变化,然后在数据发生变化时创建一个新的 `Command` 对象,并将其添加到 `CommandManager` 中。在需要撤销操作时调用 `CommandManager` 的 `undo` 方法,需要操作时调用 `CommandManager` 的 `redo` 方法。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值