w3ctech

聊聊DOM树相关的那些事件

注:增加更新mutation observer相关的内容/表格内容简单进行了翻译@20140523

问题

很多时候,我们都会遇到一个头疼的问题,如何监听页面模块DOM的变化。比如页面模块使用了类似datalazyload这样的组件,什么时候能知道这个模块的内容渲染成dom,并可以对其进行操作。

通常的解决方案都是datalazyload本身实例化的时候,提供一些callback处理。

类似:

var D = new DataLazyload();
D.addCallback('moduleId', callback);
D.on('module-load-event:moduleId', callback);

前提是,你能拿到DataLazyload的实例。

还有没有别的方式来解决这个问题呢?

这个时候,可以看下浏览器做了哪些事情。

Mutation Events

<table> <tbody> <tr> <td>事件名</td> <td>规范地址</td> <td>描述</td> </tr> <tr> <td>DOMActivate</td> <td>W3C Draft</td> <td>button、a链接等元素状态变化时触发。</td> </tr> <tr> <td>DOMAttrModified</td> <td>W3C Draft</td> <td>节点attr属性发生变化时触发</td> </tr> <tr> <td>DOMAttributeNameChanged</td> <td>W3C Draft</td> <td>节点的namespaceURI或者nodeName变化时触发(document.renameNode()这样古老的方法)</td> </tr> <tr> <td>DOMCharacterDataModified</td> <td>W3C Draft</td> <td>节点内部文本变化时触发</td> </tr> <tr> <td>DOMContentLoaded</td> <td>HTML5</td> <td> domready</td> </tr> <tr> <td>DOMElementNameChanged</td> <td>W3C Draft</td> <td>element节点namespaceURI或者nodeName变化时触发(document.renameNode()这样古老的方法)</td> </tr> <tr> <td>DOMFocusIn</td> <td>W3C Draft</td> <td> 节点出现focus后触发。</td> </tr> <tr> <td>DOMFocusOut</td> <td>W3C Draft</td> <td>节点出现focusout后触发</td> </tr> <tr> <td>DOMNodeInserted</td> <td>W3C Draft</td> <td>发生节点插入时触发</td> </tr> <tr> <td>DOMNodeInsertedIntoDocument</td> <td>W3C Draft</td> <td>文档中出现节点插入时触发</td> </tr> <tr> <td>DOMNodeRemoved</td> <td>W3C Draft</td> <td>节点本身从父节点中移除时触发</td> </tr> <tr> <td>DOMNodeRemovedFromDocument</td> <td>W3C Draft</td> <td>文档中出现节点移除时触发</td> </tr> <tr> <td>DOMSubtreeModified</td> <td>W3C Draft</td> <td>子节点产生修改时触发</td> </tr> <tr> <td></td> </tr> </tbody> </table>

表格资料来自: MDN documentation.

其中可以看到最熟悉的DOMContentLoaded,也就是标准的domready事件。

下面是一个DOMSubtreeModified相关的例子

// 监听document内部dom树的变化,比如插入/删除节点等
document.addEventListener("DOMSubtreeModified",     function(e) {
// 有变化时触发
     console.warn("change!", e);
}, false);
// 往body里插入节点的测试
var a = document.createElement("a");
document.body.appendChild(a);
/*
 输出:
 {
     ADDITION: 2,
     MODIFICATION: 1,
     REMOVAL: 3,
     attrChange: 0,
     attrName: "",
     defaultPrevented: false,
     newValue: "",
     prevValue: "",
     relatedNode: null,
     initMutationEvent: initMutationEvent(),
     bubbles: true,
     cancelable: false,
     constructor: MutationEvent { MODIFICATION=1, ADDITION=2, REMOVAL=3},
     currentTarget: Document en,
     eventPhase: 3,
     explicitOriginalTarget: body.home,
     isTrusted: true,
     originalTarget: body.home,
     target: body.home,
     timeStamp: 0,
     type: "DOMSubtreeModified"
 }
 */

还可以监听节点属性的改变。

   document.getElementById("slideshowImage").addEventListener("DOMAttrModified", function(e) {
 console.warn(e.attrName + " changed from ", e.prevValue," to: ", e.newValue);
}, false);

attrName对应熟悉名,preValue对应修改前的属性,newValue对应修改后的属性。

现状

一切都看起来很美好,可惜这个规范已经被抛弃了,原本已经实现这些事件的浏览器也渐渐不再支持。

原因很简单,类似el.innerHTML = 'xxx',这样的操作,有多少个元素在这次操作中被删除,就会触发多少次remove事件,这本身对浏览器就是一个巨大的负担。

绕了一圈回来之后,似乎只有使用一些愚蠢一些的替代方案了。

比如我们要对一个元素进行操作的时候,需要知道这个元素是否已经渲染完成。

或者我们可能会需要对A元素的子元素进行操作,那么,我们就需要知道A元素的子元素是否可以操作了。

available: function (id, callback) {
    var loopStopFlag = false;
    setTimeout(function () {
        loopStopFlag = true;
    }, 5000);
    setTimeout(function () {
        if (doc.getElementById(id)) {
            callback();
        } else if (!loopStopFlag) {
            setTimeout(arguments.callee, 50);
        }
    }, 50)
}

而对于元素属性修改的监听,就只能基于浏览器api再进行一层包装。

var attr = function(el, attrKey, attrValue){
    var attrName = attrKey; //属性名
    var prevValue = el.getAttribute('attrKey'); //旧值
    var newValue = attrValue; //新值
    //xxx
}

这个时候,统一编码规范的重要性就体现出来了,通过使用统一提供的API,我们一样能实现对各个操作的监听,比如jQuery如果愿意做的话,也是可以轻易实现这些操作的监听的。

笔者知识面有限,如果你有这方面相关的更好的想法,欢迎讨论。

未来,Mutation observer

微博中有同学提醒,还可以用mutation observer来监听dom树的变化。

恰巧云飞同学已经把MDN里的mutation observer部分的文档汉化了。

首先,我们需要看下,为什么规范抛弃了mutation events之后,又搞了一个mutation observer

看MDN里对mutation observer构造函数的定义:

MutationObserver(
      function callback
);
参数

<dl style="color: #4d4e53;"> <dt>callback</dt> <dd>该回调函数会在指定的DOM节点(目标节点)发生变化时被调用.在调用时,观察者对象会传给该函数两个参数,第一个参数是个包含了若干个MutationRecord对象的数组,第二个参数则是这个观察者对象本身.</dd> </dl>

看起来新的规范实现了类似事件代理类似的方式,统一进行触发,而不是以节点为单位触发。

之前提到mutation events的缺点就是,一次innerHTML导致1000个节点被删除的时候,会触发1000次remove事件。

而mutation observer则实现了一次操作,触发一次DOM变化事件,并把这次操作相关的DOM修改统一返回。

简单的例子:

var observer = new MutationObserver(function(records){
    // records修改记录
});
var options = {
    'childList': true,
    'attributes':true
} ;
observer.observe( document.body, options );

records里包含了修改所在的节点相关信息:

  • type:观察的变动类型(attribute、characterData或者childList)。
  • target:发生变动的DOM对象。
  • addedNodes:新增的DOM对象。
  • removedNodes:删除的DOM对象。
  • previousSibling:前一个同级DOM对象,如果没有则返回null。
  • nextSibling:下一个同级DOM对象,如果没有则返回null。
  • attributeName:发生变动的属性。如果设置了attributeFilter,则只返回预先指定的属性。
  • oldValue:变动前的值。这个属性只对attribute和characterData变动有效,如果发生childList变动,则返回null。

关于mutation observer相关的资料,除了MDN之外,还建议看下JavaScript标准参考教程-Mutation Observer

参考资料

前端圈微信

扫码关注前端圈微信公众号

共收到2条回复

  • 支持小豪!

    回复此楼
  • 支持

    回复此楼