低开开发笔记(八): 低代码编辑器实现撤销回退(命令模式,防抖处理)
好家伙,
0.代码已开源
https://github.com/Fattiger4399/ph_questionnaire-.git
1.事件触发
我们先从事件的触发开始讲起
大致上我们有两个思路可以选择
1.监控用户行为
2.监控数据变化
两种选择都会有较难处理的部分,这里我们先选第二个选项
关于监控数据,首先你会想到什么?
没错,watch
watch: {
formTemplate: {
handler: function (oldVal, newVal) {
if (!this.ischange) {
// debugger
console.log(oldVal, newVal)
}
},
deep: true,
immediate: true,
}
},
但是,这会出现一些问题
深度监视
来看看我们数据的样子
如果我们从数据的角度出发观察变化,在拖拽的过程中,
数据由
{
"list": [],
"config": {
"labelPosition": "top",
"labelWidth": 80,
"size": "mini",
"outputHidden": true,
"hideRequiredMark": false,
"syncLabelRequired": false,
"labelSuffix": "",
"customStyle": ""
}
}
变成了
{
"list": [
{
"type": "input",
"options": {
"defaultValue": "",
"type": "text",
"prepend": "",
"append": "",
"placeholder": "请输入",
"maxLength": 0,
"clearable": false,
"hidden": false,
"disabled": false
},
"label": "输入框",
"labelWidth": -1,
"width": "100%",
"span": 24,
"model": "input_17211185804812",
"key": "input_17211185804812",
"rules": [
{
"required": false,
"message": "必填项",
"trigger": [
"blur"
]
}
],
"dynamicLabel": false
}
],
"config": {
"labelPosition": "top",
"labelWidth": 80,
"size": "mini",
"outputHidden": true,
"hideRequiredMark": false,
"syncLabelRequired": false,
"labelSuffix": "",
"customStyle": ""
}
}
由于监控的是一个复杂对象,这会导致watch多次触发
2.防抖
function debounce(func, wait) {
let timeout;
return function () {
const context = this;
const args = arguments;
clearTimeout(timeout);
timeout = setTimeout(() => {
func.apply(context, args);
}, wait);
};
}
watch: {
formTemplate: {
handler: debounce(function (oldVal, newVal) {
if (!this.ischange) {
this.undoStack.push(deepClone(oldVal))
}
}, 300),
deep: true,
immediate: true,
}
},
3.栈实现撤回
这里我们使用栈去做状态记录的保存
handleUndo() {
this.ischange = true
if (this.undoStack.length > 1) {
let laststate = this.undoStack[this.undoStack.length - 2]
this.formTemplate = deepClone(laststate)
let redostate = this.undoStack.pop()
this.redoStack.push(redostate)
} else {
alert("撤回栈已空,无法撤回")
}
setTimeout(() => {
this.ischange = false
}, 400)
},
handleRedo() {
if (this.redoStack.length > 0) {
this.formTemplate = this.redoStack.pop()
} else {
alert("无法重做")
}
},
-
撤销操作:
- 将当前状态保存到重做栈中。
- 从撤销栈中取出最后一个状态,并将其设为当前状态。
- 从撤销栈中移除最后一个状态。
-
重做操作:
- 将当前状态保存到撤销栈中。
- 从重做栈中取出最后一个状态,并将其设为当前状态。
- 从重做栈中移除最后一个状态。
逻辑图
过程解释
-
初始状态:
- 空白的工作区。
- 撤销栈是空的。
- 重做栈是空的。
-
用户进行第一个操作:
- 用户在工作区添加了“元素一”。
- 撤销栈中保存了操作前的状态(空白)。
- 重做栈依然是空的。
-
用户进行第二个操作:
- 用户在工作区添加了“元素二”。
- 撤销栈中保存了操作前的状态(元素一)。
- 撤销栈现在有两个状态(元素一和空白)。
- 重做栈依然是空的。
-
用户点击撤回:
- 撤回上一步操作,恢复到上一个状态(元素一)。
- 撤销栈中移除最后一个状态(元素二),撤销栈现在只有一个状态(空白)。
- 重做栈中保存被撤销的状态(元素二)。
-
用户点击重做:
- 重做上一步撤销的操作,恢复到上一个状态(元素一)。
- 撤销栈中保存恢复前的状态(空白)。
- 重做栈移除最后一个状态(元素一),现在只有一个状态(元素二)。
4.使用命令模式思想封装
最后,我们对代码进行封装
//命令类
class Command {
constructor(execute, undo) {
this.execute = execute;
this.undo = undo;
}
}
class UndoCommand extends Command {
constructor(context) {
super(
() => {
if (context.undoStack.length > 1) {
let laststate = context.undoStack[context.undoStack.length - 2];
context.formTemplate = deepClone(laststate);
let redostate = context.undoStack.pop();
context.redoStack.push(redostate);
} else {
alert("撤回栈已空,无法撤回");
}
setTimeout(() => {
context.ischange = false;
}, 400);
},
() => {
if (context.redoStack.length > 0) {
context.formTemplate = context.redoStack.pop();
} else {
alert("无法重做");
}
}
);
}
}
class RedoCommand extends Command {
constructor(context) {
super(
() => {
if (context.redoStack.length > 0) {
context.formTemplate = context.redoStack.pop();
} else {
alert("无法重做");
}
},
() => {
// 这里可以实现撤销 redo 的逻辑,但我们暂时不需要
}
);
}
}
//methods
//撤销重做
handleUndo() {
this.ischange = true;
const undoCommand = new UndoCommand(this);
undoCommand.execute();
},
handleRedo() {
const redoCommand = new RedoCommand(this);
redoCommand.execute();
},