好家伙,
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; returnfunction () { 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 = trueif(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 =newUndoCommand(this); undoCommand.execute(); }, handleRedo() { const redoCommand =newRedoCommand(this); redoCommand.execute(); },