搜索

查看: 3135|回复: 11

[JavaScript] JavaScript撤销恢复操作的实现方法详解

[复制链接]
发表于 2023-5-4 11:48:00 | 显示全部楼层 |阅读模式
Editor 2023-5-4 11:48:00 3135 11 看全部
目录
  • 前言
  • 一、初期设想
  • 二、如何收集状态
  • 1.通信尝试
  • 2.如何通信
  • 三、管理者与执行者
  • 1.数据驱动
  • 2.管理者
  • 3.执行者
    前言
    这是一个基于原生JavaScript+Three.js的系统, 我需要在里面增加撤销恢复的功能, 这并非针对一个功能, 而是各种操作.
    主要记录思路.

    一、初期设想
    栈似乎很合适, 用栈存储状态.
    最近的一次操作入栈存在于栈顶, 而撤销操作只需要对栈顶的状态进行操作, 这遵循栈的后进先出原则(LIFO).
    然后再设置一系列方法去操作栈, 增加一点安全性.
    首先是各种功能应该在什么地方发起出入栈操作, 这里有一大堆js文件.
    其次对于不同的功能需要收集什么参数来组织状态入栈.
    不同的功能出栈应该分别进行什么操作, 回撤出栈肯定要调这堆文件里的方法来把老状态填回去.

    二、如何收集状态
    先写个demo,我建了一个数组栈放在class Manager, 设置了入栈方法push, 出栈方法pop以及查看栈顶的peek方法.
    class Manager {
        constructor() {
            this.stats= [];
        }
        push(action) {
            this.stats.push(action);
        }
        pop() {
            const nowAction = this.doActions.pop();
        }
        peek(which) {
            return this.doActions[this.doCount];
        }
    }
    export { Manager }
    收集状态就不得不去满地的找了, 操作都写好了, 还是不要乱动.
    除非单独写一套独立的逻辑, 执行系统的同时也执行我自己的, 但这基本是要重写一套系统了(

    1.通信尝试
    但还是要想办法把各处的数据都划拉到Manager里.
    呃, 老实说我并没有什么原生开发的经验, 我在多个文件里引入了Manager类并且期望着这些文件可以基于Manager建立联络实现数据共享, 比如在a.js和b.js内:
    只是举个例子, 不要用一个字母去命名文件.
    // a.js
    import { manager } from "./backup/manager.js";
    const manager = new Manager();
    const action = {
      name: 'saveWorldList',
      params: {
        target: '108',
        value: [
          world: {
            psr: {},
            annotation: {}
          }
        ]
      }
    }
    for (let i = 0; i
    // b.js
    import { manager } from "./backup/manager.js";
    const manager = new Manager();
    const undoAction = manager.pop();
    console.log(undoAction);
    然而这样做并不能实现数据共享, 每一个刚刚实例化出来的对象都是崭新的.
    const manager = new Manager();
    只是使用原始干净的class Manger实例化了一个仅存在于这个模块里的对象manager.

    2.如何通信
    如果将一个对象放在公用的模块里, 从各个文件触发去操作这一个对象呢…公用模块里的数据总不至于对来自不同方向的访问做出不同的回应吧?
    class Manager {
        constructor() {
            this.stats= [];
        }
        push(action) {
            this.stats.push(action);
        }
        pop() {
            const nowAction = this.doActions.pop();
        }
        peek(which) {
            return this.doActions[this.doCount];
        }
    }
    const manager = new Manager();
    export { manager }
    之后分别在各个js文件引入manager, 共同操作该模块内的同一个manager, 可以构成联系, 从不同位置向manager同步数据.
    manager几乎像服务器里的数据库, 接受存储从各处发送的数据.

    三、管理者与执行者
    现在入栈方案基本确定了, 一个入栈方法push就能通用, 那出栈怎么办呢.
    不同的操作必须由不同的出栈方法执行.
    最初设想是写一个大函数存在class manager里, 只要发起回撤就调这个函数, 至于具体执行什么, 根据参数去确定.
    但是这方法不太好.
    首先, 我会在用户触发ctrl + z键盘事件时发起回撤调用回撤函数, 但是我只在这一处调用, 如何判定给回撤函数的参数该传什么呢? 如果要传参, 我怎么在ctrl + z事件监听的地方获取到该回撤什么操作以传送正确的参数呢?
    另外, 如果这样做, 我需要在manager.js这一个文件里拿到所有回撤操作需要的方法和它们的参数, 这个项目中的大部分文件都以一个巨大的类起手, 构造函数需要传参, 导出的还是这个类, 我如果直接在manager里引入这些文件去new它们, 先不说构造函数传参的问题, 生成的对象是崭新的, 会因为一些方法没有调用导致对象里的数据不存在或者错误, 而我去使用这些数据自然也导致错误.
    我最好能拿到回撤那一刻的数据, 那是新鲜的数据, 是有价值的.
    另外manager会在许多地方引入, 它最好不要太大太复杂.

    1.数据驱动
    传参的方案十分不合理, 最好能用别的方法向回撤函数描述要执行怎样的回撤操作.
    在入栈的时候直接于数据中描述该份数据如何进行回撤似乎也行, 但是以字符串描述出来该如何执行?
    用switch吗, 那需要在回撤函数内写全部处理方案, 哪怕处理方案抽离也需要根据switch调取函数, 就像这样:
    class Manager {
      constructor () {
        this.stats = [];
      }
      pop() {
        const action = this.stats.pop();
        switch (action) {
              planA:
            this.planAFun(action.params);
          break;
          planB:
            this.planBFun(action.params);
          break;
          // ...
        }
      }
    }
    将来万一要加别的功能的回撤, 一个函数百十行就不太好看了, 还是在类里面的函数.
    那…把switch也抽出去? 似乎没必要.

    2.管理者
    参考steam, 嗯, 就是那个游戏平台)
    steam可以看作游戏的启动器吧, 抛开人工操作, 如果需要启动游戏,那么先启动steam, steam再去启动游戏, steam可以看作一个管理者.
    管理者只需要去决定, 并且调用分派事项给正确的执行者就好, 管理者自己不执行.
    参考你老板.
    然后Manager可以作为这样一个角色, 它只负责维护状态和分配任务:
    import { Exec } from './exec.js';
    import { deepCopy } from "../util.js";
    const executors = new Exec(); // 执行者名单
    class Manager {
      constructor() {
        this.editor = null;
        this.doCount = 0;
        this.doActions = [];
        this.undoCount = 0;
        this.undoActions = [];
        this.justUndo = false;
        this.justRedo = false;
      }
      do(action) { // 增加状态
        if (this.justUndo || this.justRedo) { // undo/redo后, world不应立即入栈
          this.justUndo === true && (this.justUndo = false);
          this.justRedo === true && (this.justRedo = false);
          return;
        }
        this.previousWorld = action.params.value;
        this.doActions.push(action);
        this.doCount++
        console.log("Do: under control: ", this.doActions);
      }
      undo() { // 回撤事项分配
        if (this.doActions.length === 1) {
          console.log(`Cannot undo: doSatck length: ${this.doActions.length}.`);
          return;
        }
        const nowAction = this.doActions.pop();
        this.doCount--;
        this.undoActions.push(nowAction);
        this.undoCount++;
        const previousAction = this.peek('do');
        const executor = this.getFunction(`${previousAction.name}Undo`);
        executor(this.editor, previousAction.params)
        this.justUndo = true;
        console.log(`Undo: Stack now: `, this.doActions);
      }
      redo() { // 恢复事项分配
         if (this.undoActions.length === 0) {
           console.log(`Connot redo: redoStack length: ${this.undoActions.length}.`);
           return;
         }
        const nowAction = this.undoActions.pop();
        this.undoCount--;
        this.doActions.push(nowAction);
        this.doCount++;
        const previousAction = nowAction;
        const executor = this.getFunction(`${previousAction.name}Redo`);
        executor(this.editor, previousAction.params);
        this.justRedo = true;
        console.log(`Redo: Stack now: `, this.doActions);
      }
      getFunction(name) {
        return executors[name];
      }
      reset() { // 重置状态
        this.doCount = 0;
        this.doActions = [];
        this.undoCount = 0;
        this.undoActions = []
      }
      peek(which) { // 检视状态
        if (which === 'do') {
          return this.doActions[this.doCount];
        } else if (which === 'undo') {
          return this.undoAction[this.undoCount];
        }
      }
      initEditor(editor) {
        this.data = editor;
      }
    }
    const manager = new Manager();
    export { manager }
    justUndo/justRedo, 我的状态收集是在一次请求前, 这个请求函数固定在每次世界变化之后触发, 将当前的世界状态上传. 所以为了避免回撤或恢复世界操作调用请求函数将回撤或恢复的世界再次重复加入栈内而设置.
    undo或者redo这两种事情发生后, 执行者manager通过原生数组方法获取到本次事项的状态对象(出栈), 借助getFunction(看作它的秘书吧)访问执行者名单, 帮自己选取该事项合适的执行者, 并调用该执行者执行任务(参考undo, redo函数体).
    执行者名单背后是一个函数库一样的结构, 类似各个部门.
    这样只需要无脑undo()就好, manager会根据本次的状态对象分配执行者处理.
    do这个操作比较简单也没有多种情况, 就没必要分配执行者了…

    3.执行者
    执行者名单需要为一个对象, 这样getFunction()秘书才能够为manager选出合适的执行者, 执行者名单应为如下结构:
    // 执行者有擅长回撤(undo)和恢复(redo)的两种
    {
      planA: planAFun (data, params) {
        // ...
      },
      planAUndo: planAUndoFun (data, params) {
        // ...
      },
      planB: planBFun () {
        // ...
      },
      planBUndo: planBUndoFun (data, params) {
        // ...
      }
      ...
    }
    也好, 那就直接把所有执行者抽离为一个类, 实例化该类后自然能形成这种数据结构:
    class Exec { // executor
      saveWorldRedo (data, params) {
        // ...
      }
      saveWorldUndo (data, params) {
        // ...
      }
      initialWorldUndo (data, params) {
        // ...
      }
    }
    export { Exec };
    实例化后:
    {
      saveWorldRedo: function (data, params) {
        // ...
      },
      saveWorldUndo: function (data, params) {
        // ...
      },
      initialWorldUndo: function (data, params) {
        // ...
      }
    }
    正是需要的结构.
    getFunction可以由解析状态对象进而决定枚举executor对象中的哪个执行者出来调用:
    const executor = getFunction (name) {
      return executors[name];
    }
    到此这篇关于JavaScript撤销恢复操作的实现方法详解的文章就介绍到这了,更多相关JS撤销恢复操作内容请搜索知鸟论坛以前的文章或继续浏览下面的相关文章希望大家以后多多支持知鸟论坛
  • 回复

    使用道具 举报

    发表于 2023-6-29 08:05:51 | 显示全部楼层
    塞翁364 2023-6-29 08:05:51 看全部
    这个帖子不回对不起自己!我想我是一天也不能离开知鸟论坛
    回复

    使用道具 举报

    发表于 2023-6-29 20:41:37 | 显示全部楼层
    心随674 2023-6-29 20:41:37 看全部
    楼主太厉害了!楼主,I*老*虎*U!我觉得知鸟论坛真是个好地方!
    回复

    使用道具 举报

    发表于 2023-6-30 00:07:02 | 显示全部楼层
    123456823 2023-6-30 00:07:02 看全部
    我看不错噢 谢谢楼主!知鸟论坛越来越好!
    回复

    使用道具 举报

    发表于 2023-6-30 03:28:31 | 显示全部楼层
    啤酒瓶空了缓 2023-6-30 03:28:31 看全部
    其实我一直觉得楼主的品味不错!呵呵!知鸟论坛太棒了!
    回复

    使用道具 举报

    发表于 2023-6-30 10:42:18 | 显示全部楼层
    我的苦恼冉 2023-6-30 10:42:18 看全部
    既然你诚信诚意的推荐了,那我就勉为其难的看看吧!知鸟论坛不走平凡路。
    回复

    使用道具 举报

    发表于 2023-6-30 11:34:01 | 显示全部楼层
    术数古籍专卖疤 2023-6-30 11:34:01 看全部
    感谢楼主的无私分享!要想知鸟论坛好 就靠你我他
    回复

    使用道具 举报

    发表于 2023-7-6 16:40:15 | 显示全部楼层
    123456868 2023-7-6 16:40:15 看全部
    既然你诚信诚意的推荐了,那我就勉为其难的看看吧!知鸟论坛不走平凡路。
    回复

    使用道具 举报

    发表于 2023-7-6 19:32:25 | 显示全部楼层
    米老鼠和蓝精鼠v 2023-7-6 19:32:25 看全部
    既然你诚信诚意的推荐了,那我就勉为其难的看看吧!知鸟论坛不走平凡路。
    回复

    使用道具 举报

    发表于 2023-7-6 20:28:21 | 显示全部楼层
    戏做顿 2023-7-6 20:28:21 看全部
    既然你诚信诚意的推荐了,那我就勉为其难的看看吧!知鸟论坛不走平凡路。
    回复

    使用道具 举报

    • 您可能感兴趣
    点击右侧快捷回复 【请勿灌水】
    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则 返回列表

    RSS订阅| SiteMap| 小黑屋| 知鸟论坛
    联系邮箱E-mail:zniao@foxmail.com
    快速回复 返回顶部 返回列表