好傢伙,
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();
},