前言
一个低代码项目遇到的撤销重做功能,基于vue3实现。
实现步骤
- 通过pinia来存储历史记录数组(arr)、目前页面所展示的对应的索引(index)、现在是否要生成快照(isSnapshot),作用是防止撤销重做的时候也会添加快照,因为撤销重做的时候会触发watch里面的方法、最大能够存储的数据数(maxStep)。在pinia定义几个方法。
- 第一个生成快照,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;
}
- 第二个判断是否溢出,溢出shift出一个元素。
// 判断是否溢出
isFull() {
if (this.snapshotData.length == this.maxStep) {
this.snapshotData.shift(0);
}
},
- 第三个撤销,如果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];
}
}
},
- 第四个重做,如果没有到数组的最尾端证明可以重做,然后把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];
}
}
}
注意点
虽然简单方便但可能会引起数据的丢失。
缺点:
- 使用JSON.Stringify 转换的数据中,如果包含 function,undefined,Symbol,这几种类型,不可枚举属性,JSON.Stringify序列化后,这个键值对会消失。
- 转换的数据中包含 NaN,Infinity 值(含-Infinity),JSON序列化后的结果会是null。
- 转换的数据中包含Date对象,JSON.Stringify序列化之后,会变成字符串。
- 转换的数据包含RegExp 引用类型序列化之后会变成空对象。
- 无法序列化不可枚举属性。
- 无法序列化对象的循环引用,(例如: obj[key] = obj)。
- 无法序列化对象的原型链。
总体代码
// 监听事件,这里会遇到频繁触动的问题,用防抖解决
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];
}
}
}
}
})