You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
$('#editor').bind('keyup', function() {
clearTimeout(timer);
timer = setTimeout(function() {
var newValue = text.html();
// ignore meta key presses
if (newValue !== startValue) {
// this could try and make a diff instead of storing snapshots
stack.execute(new EditCommand(text, startValue, newValue));
startValue = newValue;
}
}, 250);
});
完成了有关于 contentEditable 的撤销重做功能后,大家再看我写的 TodoMVC 和 Google Now 这两个例子,上升到应用的层次上的撤销重做功能!牛逼吧啊哈哈哈!我还用 Polymer 搞了一个自定义 Web Component ,我将其命名为 ,哼哼哼,将所有跟撤销重做的代码都封装到这个标签里去,然后使用的话只需要:
上面我们看了代码演示,接下来让我们快速过一下性能的问题。MO 比 DOM ME 更有效也更安全,但从速度对比上还不是很明显,它的一个核心优势是我们避免了如下例使用 DOMNodeInserted 所出现的问题:
// 千万注意: 别这么用,会傻逼的
document.addEventListener('DOMNodeInserted', function() {
var el = document.createElement('div');
document.body.appendChild(el);
});
是吧,傻逼都能看出来这么做会引起死循环,监听 DOM 插入事件,结果回调里面执行了插入 DOM 的动作,又引起事件的触发,浏览器不断的中断渲染(重新计算样式和布局),导致 FOUC 不断闪瞎你的双眼。 MO 没有这个问题,浏览器会在有价值的情况下再进行通知(例如当所有JavaScript都执行完毕之后,或在浏览器样式渲染和计算都完成以后)。
注意:我刻意避开了关于 MO 的基准点测试,因为它在正常环境下是没有什么性能问题的,一些具体的差异细节可以去 jsPerf 看看,但我不保证它的准确性。╮(╯_╰)╭
攻受之分
对于监听,我们最常讨论就是 GC 和引用的问题了,MO 闭页面会自动断开文档内所有节点的监听,它相对于目标节点都是弱引用,节点被销毁它也不会再触发事件了。但DOM节点相对于 MO 却是强引用,所以如果目标节点还存在的情况下,你应该避免在MO实例在没断开节点前而销毁它,尽可能的在在页面unload的时候再断开监听吧。
Mutation Observers 的局限性
上面我一直在夸它的样子,但它并不是没有缺点,下面说说MO的局限性:
MO 还不能准确的监听表单元素中的状态改变,因为它们的“live”或“true”并不能真正的反射到属性层,而这个"value"其实只是"defaultValue",类似的元素比如<textarea>的值或是否闭合都需要单独监听,比如input内容改变,但input.getAttribute('value')返回会一直是null。普遍的解决方案是用通过监听input的keyup事件,我经常也见到有的同学会用50-200毫秒的轮询去检测文本改变。
MO 没办法检测到CSS样式的更改(如hover状态什么的)。
变更记录里没有时间戳。
DOM ME 有个问题是直接关联于插入的DOM节点,并保持完整的调用作用域,相当耗费内存成本。所以 MO 的延迟处理和批处理的效率就更高些了。但也因此导致很多不需要的信息产生,比如插入每个节点的执行环境什么的。
原文地址:Detect, Undo And Redo DOM Changes With Mutation Observers
文中所出专有名词:
MO == Mutation Observers
ME == Mutation Events
MR == MutationRecord
这篇文章里,我会向大家解释为啥Mutation Observers(以下简称MO)这么牛逼,MO是那个发育不良的DOM Mutation Events(死慢死慢又很吃性能的家伙,以下简称ME)的替代品,它可以监听复数级的DOM的改变。可目前支持它的浏览器并不多,从ChromeStatus.com的数据来看,只有0.03%的业务使用量。所以为啥不再给她多点关注呢,好可怜的说。StackOverflow关于MO的提问大多数来自于Chrome拓展和富文本编辑器,不过我觉得还可以用在更多地方,比如,DOM修改的Undo/Redo是吧,先看下下面这个例子吧:
有木有感觉吊炸天(作者自我感觉真良好),下面我快速说下它是个什么玩意~
对比 Mutation Events 和 Mutation Observers
MO对DOM的监听方式不同于旧的ME,它是等该DOM的所有变更行为都结束之后才异步触发回调的,使得我们可以更专注的对DOM进行处理。这就好比一天撸一次和一周撸一次的区别一样,你说哪个对身体好啊!【哎?作者了什么奇怪的事情~( ⊙ o ⊙ )
如何愉悦的使用 Mutation Observers
大家都很忙(都很懒),我就提供一下基础代码片段,看下它是怎么监听DOM改变的就好了:
如果你像我一样屌丝整天构造对象实例化对象指向对象就是没对象的,那你肯定有大把的时间来做代码实践。下面是两个更好的例子,通过 contenteditable 与 el.innerHTML 去改变DOM结构,你可以清晰的看到MO是怎么异步处理和输出的啦。
http://jsbin.com/xazok/1/edit
http://jsbin.com/bajuqi/1/edit
我们可以监听到子节点的添加删除、属性和文本内容与数据的改变。再次提醒大家,MO 不是对每个改变都触发回调的,它是在DOM在进行了一组变化结束以后在堆栈空闲时才触发的,就像个记录了很多版本更新后而形成的发布日志一样。
让我们进一步分解代码,实例化的MO需要传入一个回调函数,当触发回调时它会传入一个 MutationRecord(以下简称MR) 对象的数组:
上面的例子在: http://jsbin.com/cogid/1/edit
我们通过回调得到了一个数组,里面是 MR 对象,从控制台输出的数据你可以看到,MR 除了 target 和 oldValue 以外,还有很多有用的属性:
我们可以用下面的这个 observe() 方法来注册需要监听的DOM节点,并通过options对象参数来指定哪些类型的变化可以触发回调。
可配参数有:
如上,我们可以通过 options 来配置监听我们希望被监听到的节点,即便是从root也OK,但当然,只求所需将会为我们带来更好的性能。来让我们在页面上搞出一块儿可编辑区域吧(比如: <div id="editor” contentEditable> )
首先,让我们取得它的元素引用:
然后我们用已经实例化好的 MO 来监听它
从下面的截图我们可以看出,只要可编辑区域的内容有任何变化,回调方法都会被调用,并返回 MR 对象列表。
爽不爽!
(≧▽≦)/(作者自嗨中),当然,如果我们想停止监听,只要调用disconnect()方法即可:然后所有的监听就停止了,一切风平浪静。这里我们不太深入的去了解 MO 的 API 什么的了, Dev.Opera 和 MDN 都有良好的介绍文档,有兴趣的去读读吧。下面让我们谈一下如何通过这个逆天的MO去实现 DOM 的撤销功能,噗,会很有趣的,别走开,没有广告啊马上回来。
用 Mutation Observers 实现 Undo/Redo
做过编辑器的同学都知道Undo或Redo在其中扮演着怎样重要的角色,然后懒死的浏览器厂商还都没有一个实现标准API接口的(没API标准?你搞笑呢 UndoManager),我们通常不得不通过用堆栈来存储历史数据,搞的编辑器内存负重直线上升,一桶水不满半桶水晃悠的样子。
这些傻逼大拿们都用各种框架啊React或Clojure什么的去做,光看看代码就有一种历史的厚重感,糟糕透了。那我们是不是可以尝试一下只用原生JS就能够实现的解决方案呢?虽然可能会滥用回调,可谁不是呢~
步骤一
让我们开始布置舞台,在 app.html,用 contentEditable 属性搞出一块儿可编辑区域和两个按钮 —— 撤销和重做。虽然这儿只是拿编辑文本做例子,但本质上是通用的,都是实现DOM改变的撤销与重做(所以自然也适用于任务列表的添加更改)
这是我们的HTML:
然后开始写JS:
之后我们会用为这两个按钮绑定点击事件,点击后他们会撤销或重做编辑框里的内容。
其实我们需要实现的地方有两部分:内容已经改变的时机和在撤销或重做的时候它的逻辑是什么。我们用 MO 实现第一部分,用 Undo.js 实现第二部分,这是大神Jörn Zaefferer为 Undo/Redo 特意做的一个抽象原型,我们可以像如下代码一样很方便的去用它:
步骤二 Step 2
凑巧的是 Undo.js 就有一个关于编辑器撤销重做的例子(我机智的放到了这里 JSBin),但在内容改变时机上用的是onkeyup事件,真low~,大家看看EditCommand的使用方式就好了:
最主要的一行就是那个stack.execute(new EditCommand(...)),它需要传入三个参数,所编辑的DOM节点、老的内容和新的内容。
所以,我们要先定义一下我们自己的 EditCommand。很简单,继承 Undo.Command 定义一个构造函数,undo 方法和 redo 方法简单的使用 innerHTML 赋值。
OK,搞定 EditCommand 后我们有了一个记录堆栈,让我们继续用 MO 把之前的傻逼onkeyup事件替换掉。
步骤三
首先,定义一个实例化 MO 加回调,胡乱把代码复制进去就好啦:
为了忠于原demo,我们可以简单的将 编辑器里的内容全部输出当作 newValue 传过去,那MO起的作用其实是“这里有些东西改变了”,而不是去使用它提供的具体改变后的内容。解决方案是向下面这样把每一个变化都记录下来:
无论怎样,我们都会把可编辑节点、老值和新值传入到实例化的 EditCommand 中去。
这里要注意的是,在做Undo/Redo动作时,依旧属于“内容被改变”,会重新触发MO执行回调,为了避免这个问题,我们可以加个状态判断。
记得在 EditCommand 也要把在这个 blocked 状态用上喔,不然白加了…
步骤四
下面,我们来执行监听器的 observe 方法吧~ 启动起来!
我们想确保只有在有内容改变的情况下撤销和重做按钮才可点,那么我们可以如下写这样的一个方法,通过 stack 的两个判断方法来检测现在是否可以撤销或重做。
那如下自然,每当 stack 中有改变的时候,都要执行一下stackUI方法了
步骤五
最后我们写好按钮的点击事件监听,well done~
就这样,我们实现了编辑器的撤销和重做功能,so easy,你可以查看Demo源码看看效果如何
Demo: http://jsbin.com/zamacuta/5/
注:我只是简单写写啊目前是单字符撤销,没深处理,你可以再通过一定的延迟来达到更好的效果。
虽然上面的例子可能会有一定的缺陷和性能限制,但它的确是很简易的用原生JS实现了应用的撤销和重做功能啊!嘛……即便你觉得MO没啥用,那下个 Undo.js 玩玩也不错嘛是吧哈哈……
contentEditable 之外
完成了有关于 contentEditable 的撤销重做功能后,大家再看我写的 TodoMVC 和 Google Now 这两个例子,上升到应用的层次上的撤销重做功能!牛逼吧啊哈哈哈!我还用 Polymer 搞了一个自定义 Web Component ,我将其命名为 ,哼哼哼,将所有跟撤销重做的代码都封装到这个标签里去,然后使用的话只需要:
虽然现在说这个还有点早,毕竟各浏览器平台还都没怎么实现它。但从理念上讲讲还是可以的,就是我们写了这个标签在外面之后呢就什么都不用管了!它自动生成按钮与监听DOM改变,如果你感兴趣的话可以看看这个demo Circles 。
Mutation Observers 的性能优势
上面我们看了代码演示,接下来让我们快速过一下性能的问题。MO 比 DOM ME 更有效也更安全,但从速度对比上还不是很明显,它的一个核心优势是我们避免了如下例使用 DOMNodeInserted 所出现的问题:
是吧,傻逼都能看出来这么做会引起死循环,监听 DOM 插入事件,结果回调里面执行了插入 DOM 的动作,又引起事件的触发,浏览器不断的中断渲染(重新计算样式和布局),导致 FOUC 不断闪瞎你的双眼。 MO 没有这个问题,浏览器会在有价值的情况下再进行通知(例如当所有JavaScript都执行完毕之后,或在浏览器样式渲染和计算都完成以后)。
如果你对这个话题感兴趣的话,去看 this Stack Overflow thread。
注意:我刻意避开了关于 MO 的基准点测试,因为它在正常环境下是没有什么性能问题的,一些具体的差异细节可以去 jsPerf 看看,但我不保证它的准确性。╮(╯_╰)╭
攻受之分
对于监听,我们最常讨论就是 GC 和引用的问题了,MO 闭页面会自动断开文档内所有节点的监听,它相对于目标节点都是弱引用,节点被销毁它也不会再触发事件了。但DOM节点相对于 MO 却是强引用,所以如果目标节点还存在的情况下,你应该避免在MO实例在没断开节点前而销毁它,尽可能的在在页面unload的时候再断开监听吧。
Mutation Observers 的局限性
上面我一直在夸它的样子,但它并不是没有缺点,下面说说MO的局限性:
最后
(以上翻译内容都是我瞎编的,终于要完事儿了,我的灵感也快枯竭了)
有些人可能认为,毕竟是比较新鲜的玩意,不稳定啊时机还不太成熟,还不太敢使用它。但从下面的图中大家可以看到 MO 在浏览器中的支持还是不错的,虽然或多或少都有些缺陷,还是值得一试的。
一如既往,如果你有关于这个API的问题欢迎向 Blink 团队反馈,我很乐于看到你们为本文指出错误,当然,改不改就是我的事情了。希望此篇文章对你有所帮助。感谢 Mathias Bynens 和 Pascal Hartig 的提前浏览,没他们我肯定会写的更好,随时欢迎反馈和点评,反正我不会再动这篇文章了,哼~
The text was updated successfully, but these errors were encountered: