- 前端页面上实现undo-redo功能
一、引语 undo/redo的操作相信大家都不陌生,例子有很多,比如:编辑器的undo/redo、Word的undo/redo、数据库的undo log/redo log 等等。 本文将用原生JS构造一个undo/redo以至于在前端操作中也能达到撤销重做的效果,还会涉及在vue中如何使用undo/redo以及Immutable能给undo/redo带来怎样的好处。 二、undo/redo简单版本 首先分解undo/redo的功能,我们至少要需要三个东西: 一个操作,最好是有逆操作的,比如删除的逆操作就是插入 能够存放操作方法和逆操作的方法的数据结构 提供操作undo/redo的接口 一个简单的例子: [图片] 图中是一个“隐藏元素”的操作,这个操作的逆操作就是“显示元素”。 接下来考虑使用什么数据结构存放操作, 因为不仅有一个操作,还有移动元素的位置、改变元素的颜色等等操作,但最后执行的操作肯定是在最前面被undo的,也就很容易联想到存放操作方法采用的数据结构是一个栈。 [代码]class Stack { constructor () { this.stack = []; } push (item) { return this.stack.push(item); } pop () { return this.stack.pop(); } isEmpty () { return this.stack.length === 0; } } [代码] 我们还需要定义一个undoable的方法,代表这个操作是可以被undo的。 undoable方法传入主要传入几个参数:执行undo的function,执行redo的function,undo function的参数,redo function的参数,这几个参数保证每次操作的时候把执行过程保留下来(保护现场),并且保存对应逆操作的方法,使得undo按钮的操作只是把undo stack弹出最新的方法并且执行即可,redo按钮的操作同理。 [代码]const preview = document.getElementById('preview'); const btn = document.getElementById('btn'); const undoBtn = document.getElementById('undo'); const redoBtn = document.getElementById('redo'); const undoRedo = new UndoRedo(); undoBtn.addEventListener('click', function () { undoRedo.undo(); }) redoBtn.addEventListener('click', function () { undoRedo.redo(); }) const hideElCommand = HideElCommand.of(preview)(undoRedo); btn.addEventListener('click', function () { hideElCommand.execute().undoable(); }); [代码] 万事俱备,只差一个UndoRedo的类了,简单的实现如下: [代码]class UndoRedo { constructor () { this.undoStack = new Stack(); this.redoStack = new Stack(); } undoable (options) { const { thisArg = null, undoFn = noop, redoFn = noop, undoArgs = [], redoArgs = [], } = options; this.undoStack.push( { undoFn: undoFn.bind(thisArg, undoArgs), redoFn: redoFn.bind(thisArg, redoArgs), } ); } undo () { if (this.undoStack.isEmpty()) { return; } const obj = this.undoStack.pop(); typeof obj.undoFn === 'function' && obj.undoFn(); this._addRedo(obj); } redo () { if (this.redoStack.isEmpty()) { return; } const obj = this.redoStack.pop(); typeof obj.redoFn === 'function' && obj.redoFn(); this._addUndo(obj); } _addRedo (redoItem) { this.redoStack.push(redoItem); } _addUndo (undoItem) { this.undoStack.push(undoItem); } } [代码] 三、带命令模式的undo/redo 上面的例子实现了一个简单的undo/redo的操作,但是写起来较麻烦,如果有好几个地方都有用到隐藏元素的操作,代码并不能很好地复用,也不够清晰,更胜一筹的做法是定义一个隐藏元素的命令,按钮的操作只需要知道执行隐藏元素的命令,并且可以定义这个命令是否可撤销的。 首先来看命令模式的作用:既不用知道调用者也不需要知道接收者,但这个命令的过程是通用的,这就很好地解耦了调用者和接收者,同时这个命令过程是可以被复用的。 回到代码中来,给命令定义一个名为undoable的方法,代表这个命令是可以被撤销的,与undoRedo对象相结合,当命令执行的时候,同时在undo/redo栈里存放着命令的过程和命令逆操作的过程。 可以抽象出一个Command父类,提供undoable, execute方法,所有定义的命令都继承于这个类: [代码]class Command { constructor (receiver, data) { this.receiver = receiver; this.data = data; this.undoRedo = null; this.undoRedoOptions = {}; } execute () { throw Error("please implement execute function!"); } undo () { throw Error("please implement undo function!"); } initUndoRedo (undoRedo, options) { this.undoRedo = undoRedo; this.undoRedoOptions = options; return this; } undoable () { const command = this; this.undoRedo.undoable({ undoFn: command.undo, redoFn: command.execute, thisArg: command, ...this.undoRedoOptions, }); return command; } } [代码] 给隐藏元素操作定义一个HideElCommand命令对象, 使用的时候很简单,直接new 一个HideElCommand.execute().undoable() [代码]class HideElCommand extends Command { constructor (receiver, data) { super(receiver, data); } execute () { this.receiver.style.display = 'none'; return this; } undo () { const showElCommand = ShowElCommand.of(this.receiver, this.data)(this.undoRedo, this.undoRedoOptions); showElCommand.execute(); return this; } static of (receiver, data) { return function (undoRedo, options) { return new HideElCommand(receiver, data).initUndoRedo(undoRedo, options); } } } const hideElCommand = HideElCommand.of(preview)(undoRedo); btn.addEventListener('click', function () { hideElCommand.execute().undoable(); }); [代码] ps. 图中把new HideElCommand的过程封装成一个of方法 四、vue中的undo/redo vue中最重要的思想:数据驱动视图,改变数据即可自动更新视图,所以只需要在UndoRedo栈中存放数据的快照即可。 实现过程也比较简单, 创建一个UndoPlugin, mixin的data中存放undoRedo对象, 同时绑定Vue原型上$undoable, $undo, $redo方法,思路和用原生js写的差不多, 具体代码就不列出来了,使用的方式如下图: [代码]export default { name: 'undo', data () { return { show: true } }, methods: { hideEl () { this.$undoable({ undo: { show: this.show }, redo: { show: !this.show } }) this.show = false } }, undo () { this.$undo() }, redo () { this.$redo() } } [代码] 但是数据的来源也可能是vuex中的state, 那么vuex存放的state怎么做undo?之前谈到一个操作最好有对应的逆操作,这样好实现undo/redo,那不可逆的操作怎么办? 以vuex实现undo/redo来举例,如果外部都是通过mutation来修改vuex的state的,就可以维护一个mutation历史的数组,undo的时候删除最后一个mutation, 重新按顺序执行muation数组的每一项即可。 ps. vuex的subscribe方法可以订阅mutation 五、Immutable 例子中的数据都是值类型的,如果数据是引用类型的,在undo/redo栈里也是会被修改的,我们需要的是一个能够存在栈里的数据快照,是不变的,由于数据是不可变的,并且我们的视图遵循UI = F(data),所以一份数据映射一个视图,改变了数据就相当于是另一份数据,只要我们把上一份数据保存起来,就能很好地回溯到上一份视图,这就涉及一个概念: Immutable(不可变的)。 以Immutable.js为例 [代码]function execute () { let data = Immutable.formJS({ a: { b: 1}}) run(data) console.log(data.getInt(['a', 'b'])) //能够清晰知道是1 } [代码] 但是这也是有弊端的,Immutable.js使用起来对代码的侵入性还是挺大的,思维上也需要转变。 Immutable.js每次都是生成新的对象,性能问题怎么解决?它的底层用的是可持久化的数据结构和structural sharing的技术,简单来说就是不变的数据节点用的都是同一份,变化的才会重新生成节点重新进行组装。 具体可以浏览这篇文章: Immutable.js, persistent data structures and structural sharing 六、总结 一开始我们讲了undo/redo的场景,和实现的三个必要条件: 操作的定义/逆操作的定义 存放操作的数据结构 提供undo/redo接口 并且实现了一个简单版本的undo/redo。 但是这个简单版本的实现不够清晰通用,于是结合了命令模式实现了第二个版本,这个版本调用方只需要知道存在一个叫做隐藏元素命令,并且这个命令是可以被undo的,只需要执行execute方法即可,有效地提高了清晰度和易用性。 最后我们粗略地讲了三点: vue、vuex的undo/redo,与简单版本最大的不同是栈中存放的是数据 不可逆的操作是如何实现undo/redo的(保存操作的历史) 介绍了Immutable的数据对于undo/redo的实现提供了极大的便利。 使用Immutable的数据确实很容易实现Undo/Redo的功能,但是Immutable对代码的侵入型较强,新项目可以尝试,大多数情况还是推荐带命令模式的undo/redo。 七、参考资料 《JavaScript设计模式与开发实践》,曾探,2015-5 Immutable.js, persistent data structures and structural sharing Immutable 详解及 React 中实践
2019-06-06 - 微信开发工具 小程序 保存+编译 没反应
微信开发工具开发小程序时,经常经常经常会遇到这种情况: 修改wxml、wxcss或js,保存(ctrl+s或者ctrl+shift+s)然后点击“编译”,页面还是没变化,需要重启微信开发工具才有用。 微信开发工具已经更新到最新,卸载后重新安装也试过了都不行 网上查了一堆都没有解决办法,特别影响开发效率,望解决!
2017-11-13