低開開發筆記(八): 低程式碼編輯器實現撤銷回退(命令模式,防抖處理)

养肥胖虎發表於2024-07-16

好傢伙,

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

相關文章