Mutation 事件
DOM Level 3 提供了一系列的 mutation 事件,如 DOMAttrModified
、DOMNodeInserted
,供开发者观察 DOM 的变动。这些事件虽然不太常用,但却非常方便好用。比如在使用 ajax 往 DOM 添加新元素时,需要检查总高度超过了容器,常见的方法是在 ajax 完成时的回调函数里执行,但这样并不利于组织代码,甚至回调函数会很臃肿;如果使用 DOMNodeInserted
就好多了:
$container.on( 'DOMNodeInserted', function() {
if ( children_height > $container.height() ) {
// do something
}
})
$.ajax({
success: function( data ) {
$container.append( $( data.html ) )
}
})
这样职责就清晰很多—— ajax 的回调函数仅处理返回的数据。
但 mutation 事件的性能太糟了,Chrome 也未必吃得消,而 Mozilla 在 Performance best practices in extensions 里也建议尽可能别用 mutation 事件。
因此 mutation 事件的处境很尴尬,能用的只有它,但又不能用,最终也被 DOM Level 4 引入的 MutationObserver
取代了。
MutationObserver
先看以下代码:
<ul></ul>
<script>
var ul = document.querySelector('ul')
, observer
try {
observer = new ( window.MutationObserver ||
window.WebKitMutationObserver )( function( mutationRecord,
observer ) {
mutationRecord.forEach( function( mutation ) {
console.log( mutation )
})
})
observer.observe( ul, { attributes: true, childList: true } )
} catch ( ex ) {}
ul.className = 'list'
ul.appendChild( document.createElement( 'li' ) )
// 不会触发
ul.children[0].className = 'item'
// 在不需要时,停止继续观察
observer.disconnect()
</script>
目前支持的浏览器似乎只有 webkit 核心和 Firefox (14+),而在 Chrome 24 和 Safari 6.0.2 里仍属于私有实现,所以必须先判断:window.MutationObserver || window.WebKitMutationObserver
。
MutationObserver
创建实例时需要一个函数作为参数作为回调函数,该函数有两个参数,一个是包含所有被观察的 MutationRecord
实例的数组,一个是 MutationObserver
实例。
observer.observe( node, anObject )
接受两个参数,需观察的对象和事件类型。事件类型用对象的形式来赋值,如 { attributes: true, childList: true }
,这决定了什么事件会被触发,比如上述例子执行结果为:
{
addedNodes: null,
attributeName: "class",
attributeNamespace: null,
nextSibling: null,
oldValue: null,
previousSibling: null,
removedNodes: null,
target: ul.list,
type: "attributes"
}
{
addedNodes: NodeList[1],
attributeName: null,
attributeNamespace: null,
nextSibling: null,
oldValue: null,
previousSibling: #text,
removedNodes: NodeList[0],
target: ul,
type: "childList"
}
MutationObserver
和一般的 event
不一样,默认仅观察指定对象,除非指定 subtree: true
,所以更改 li
的 className
没有触发属性变动事件:
ul.children[0].className = 'list'
此外还要注意的是,被观察的对象是 node
类型,所以类似这样的操作也会触发事件:ul.appendChild( document.createTextNode( 'Hello World' ) )
。
更详细的内容请参考 MDN 的文档。
CSS 动画事件的妙用
MutationObserver
虽然实用度和性能都很优秀,但毕竟是 DOM Level 4 的东西——太新——在老一点的浏览器有没有别的代替方案呢?
如果只是需要 DOMNodeInserted
,那可以利用 CSS 动画事件实现类似的效果:
<style>
@-webkit-keyframes NodeInserted {
from {}
}
@keyframes NodeInserted {
from {
clip: rect(0px, auto, auto, auto);
}
to {
clip: rect(0px, auto, auto, auto);
}
}
li {
-webkit-animation-duration: .001s;
animation-duration: .001s;
-webkit-animation-name: NodeInserted;
animation-name: NodeInserted;
}
</style>
<ul></ul>
<script>
var nodeInserted = function( event ) {
if ( 'NodeInserted' === event.animationName ) {
console.log( 'New node inserted!' )
}
}
document.addEventListener( 'webkitAnimationStart', nodeInserted, false )
document.addEventListener( 'animationstart', nodeInserted, false )
document.querySelector( 'ul' ).appendChild( document.createElement( 'li' ) )
</script>
这个技巧利用了 CSS 动画开始时触发的 animationstart
事件,因为如果一个元素只有在 DOM 里才会开始动画。
一些需要注意的地方:
- Webkit 系列(Chrome 24 & Safari 6.0.2)要加上 prefix,事件绑定也是;
- 尽量别用
jQuery.on()
绑定事件,因为event
参数是被 jQuery 创建的,目前并不支持event.animationName
等几个属性(1.9.0); - Chrome 24 有点奇葩,只有一个空的
from {}
也会触发动画事件(其他 webkit 系没测试); - Firefox 需要比较完整并有实际效果的
keyframes
,比如示例代码里的clip
,如果想省事,可以单独用from { opacity: 1 }
,但不是所有属性都可以触发; keyframes
里的 CSS 属性要多注意,别不小心覆盖了需要的,比较安全的是opacity
和clip
;- 建议通过判断
event.animationName
来避免非预期情况; - 建议用父元素做事件代理,如
document.querySelector( 'ul' ).addEventListener( 'animationstart', nodeInserted, false )
,并酌情停止冒泡。
这个技巧可以用在任何支持 CSS 动画的浏览器上,泛用性比较高,可以参考我写的 Twitter Filter 用户脚本,做成 bookmarklet 一样可以用在别的浏览器。
补充:IE9 只支持 5 个 mutation 事件,不支持 CSS 动画,MutationObserver
目前还没支持的迹象,所以忘了 IE 系列吧。