JavaScript 基于栈和命令模式的撤销恢复操作
文章目录
- 前言
- 一、初期设想
- 二、如何收集状态
- 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 < 3; i++) {manager.push(action);
}
// 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 { // executorsaveWorldRedo (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];
}
总结
好久没有写了, 这段时间遇到的一些零碎知识都扔github了.
啧, 还是再更起来吧)
我会很高兴, 如果这篇文章有帮到你.
JavaScript 基于栈和命令模式的撤销恢复操作相关推荐
- Linux中vim的三种命令格式,及命令模式下常见的操作
目录 什么是vim 三种命令模式,以及相互转换 命令模式下的文本操作 什么是vim Vim是一个类似于Vi的著名的功能强大.高度可定制的文本编辑器,在Vi的基础上改进和增加了很多特性 三种命令模式,以 ...
- 手绘板的制作——命令模式与撤销、重制(3)
前言 我们这篇来了解下撤销.重制的功能,其实也就是 undo 和 redo,在这里我们使用命令模式去设计,若对该模式不了解的话,可以考虑看下 「关于命令模式的误区,你知道了吗」. 其实对于命令模式,我 ...
- linux的vim撤销命令,[Linux] Vim 撤销 回退 操作
在vi中按u可以撤销一次操作 u 撤销上一步的操作 Ctrl+r 恢复上一步被撤销的操作 注意: 如果你输入"u"两次,你的文本恢复原样,那应该是你的Vim被配置在Vi兼容模式 ...
- Unity命令模式, 实现撤销/反撤销
可以实现记录移动的位置, 撤销和反撤销 先上图: 记录1右 记录2上 记录3上 丢弃3上 记录3左 说思路: 每一步操作都是一个"命令" 用列表记录"命令" & ...
- 设计模式之命令模式、举例分析、通俗易懂
1. 定义 命令模式(Command):将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤销的操作 简单来说,就是类似于消费者-服务员-厨师,消费 ...
- 小菜学设计模式——命令模式
2019独角兽企业重金招聘Python工程师标准>>> 背景 外面小摊与店面的比较,你就会发现,店面似乎更加容易管理,为什么呢?因为在客户与老板自己新增了很多员工,这些员工各司其职, ...
- 请求发送者与接收者解耦——命令模式
本文转载自 :http://blog.csdn.net/lovelion/article/details/8796736 装修新房的最后几道工序之一是安装插座和开关,通过开关可以控制一些电器的打开和关 ...
- Head First设计模式读书笔记五 第六章 命令模式(单例略过)
本文示例代码材料源自Head First设计模式 以前整理自己整理的链接: https://blog.csdn.net/u011109881/article/details/59675658 极简命令 ...
- 吃烧烤之命令模式学习笔记[C++版]
#pragma once /******************************** * * 命令模式应用:烤羊肉串二 * ********************************/ ...
最新文章
- 设计模式之模板方法模式(Template Method)摘录
- Windows Forms高级界面组件-使用状态栏控件
- l源码安装mysql升级_[Linux]javaEE篇:源码安装mysql
- mysql嵌入式语句_MySQL/MariaDB 语句速查笔记
- 【数据结构与算法】之深入解析“K个逆序对数组”的求解思路与算法示例
- 将JQGrid与Spring MVC和Gson集成
- python中读取指定的行和列_Python怎么获取excle中指定行和列的值?
- wc 统计文件字节数、字符数、单词数
- Php 骰子游戏,寄娱于学第2天——PHP骰子游戏篇--优化
- linux将目录打包压缩,Linux系统文件、目录及文件系统的压缩与打包详解
- SoftIce,IDA pro强强联合!从SOFTICE中打开IDA Pro输出的map信息文件
- PostgreSQL下载安装
- ecshop 模版写php,ecshop 模板直接使用运算符
- 关于100层楼,扔两个鸡蛋,求摔碎鸡蛋的临界层的问题
- 优秀logo,最基础的设计技巧(四)
- scrapy爬取表情包使用flask搭建搜索网站
- aptx与ldac音质区别_ldac_aptx和aptx hd功能介绍及区别介绍
- 上汽董事长称不接受与华为合作自动驾驶;曝OPPO给离职员工补发年终奖,此前遭克扣;Google Play 将启用AAB格式应用...
- Android扫描条形码与二维码
- 华为ENSP模拟器CE12800 CE6800设备包