97

我有兴趣使用MutationObserver它来检测是否在 HTML 页面中的任何位置添加了某个 HTML 元素。例如,我会说我想检测是否<li>在 DOM 中的任何位置添加了任何 '。

MutationObserver到目前为止,我看到的所有示例都仅检测是否将节点添加到特定容器中。例如:

一些 HTML

<body>

  ...

  <ul id='my-list'></ul>

  ...

</body>

MutationObserver定义

var container = document.querySelector('ul#my-list');

var observer = new MutationObserver(function(mutations){
  // Do something here
});

observer.observe(container, {
  childList: true,
  attributes: true,
  characterData: true,
  subtree: true,
  attributeOldValue: true,
  characterDataOldValue: true
});

所以在这个例子中,MutationObserver设置了观察一个非常确定的容器(ul#my-list)来查看是否有任何<li>'s 附加到它上面。

如果我想不那么具体<li>,并且像这样在整个 HTML 正文中观察' 是否有问题:

var container = document.querySelector('body');

我知道它适用于我为自己设置的基本示例......但是不建议这样做吗?这会导致性能不佳吗?如果是这样,我将如何检测和衡量该性能问题?

我想也许有一个原因,所有的MutationObserver例子都是针对它们的目标容器的……但我不确定。

4

1 回答 1

258

这个答案主要适用于大而复杂的页面。

如果在页面加载/渲染之前附加,未优化的 MutationObserver 回调可能会增加几秒钟的页面加载时间(例如,5 秒到 7 秒)如果页面又大又复杂(12)。回调作为阻止进一步处理 DOM 的微任务执行,并且可以在复杂页面上每秒触发数百或数千次。大多数示例和现有库都没有考虑到这种情况,并且提供了美观、易于使用但可能很慢的 JS 代码。

  1. 始终使用devtools 分析器并尝试使您的观察者回调消耗的总 CPU 时间少于页面加载期间消耗的总 CPU 时间的 1%。

  2. 通过访问 offsetTop 和类似属性避免触发强制同步布局

  3. 避免使用复杂的 DOM 框架/库,比如 jQuery,更喜欢原生 DOM 的东西

  4. 观察属性时,使用attributeFilter: ['attr1', 'attr2']选项 in .observe()

  5. 尽可能以非递归方式观察直接父母(subtree: false)。例如,通过递归观察等待父元素,成功断开观察者,在此容器元素上附加一个新的非递归
    元素是有意义的。document

  6. 当只等待一个具有id属性的元素时,使用非常快的getElementById 而不是枚举mutations数组(它可能有数千个条目):example .

  7. 例如,如果页面上所需的元素相对较少(例如iframeobject),请使用 and 返回的实时 HTMLCollectiongetElementsByTagNamegetElementsByClassName重新检查它们,而不是枚举mutations它是否有超过 100 个元素。

  8. 避免使用querySelector特别是极慢的querySelectorAll.

  9. 如果querySelectorAll在 MutationObserver 回调中绝对无法避免,首先执行querySelector检查,如果成功,则继续querySelectorAll。平均而言,这样的组合会快很多。

  10. 如果针对 2018 之前的 Chrome/ium,请不要使用需要回调的内置 Array 方法,例如 forEach、filter 等,因为在 Chrome 的 V8 中,与经典for (var i=0 ....)循环(10-100倍慢),并且 MutationObserver 回调可能会报告复杂现代页面上的数千个节点。

  • 即使在较旧的浏览器中,由 lodash 或类似的快速库支持的替代功能枚举也可以。
  • 截至 2018 年,Chrome/ium 正在内联标准数组内置方法。
  1. 如果针对 2019 年之前的浏览器,请不要使用MutationObserver 回调中的慢速 ES2015 循环for (let v of something),除非您进行编译以使生成的代码与经典for循环一样快地运行。

  2. 如果目标是改变页面的外观,并且您有一种可靠且快速的方法来告诉正在添加的元素在页面的可见部分之外,请断开观察者并安排整个页面重新检查和重新处理通过setTimeout(fn, 0):它将在解析/布局活动的初始爆发已经完成,引擎可以“呼吸”,这可能需要一秒钟。然后,您可以使用 requestAnimationFrame 以不显眼的方式处理页面。

  3. 如果处理很复杂和/或花费大量时间,它可能会导致很长的绘制帧、无响应/卡顿,因此在这种情况下,您可以使用去抖动或类似的技术,例如在外部数组中累积突变并通过以下方式安排运行setTimeout / requestIdleCallback / requestAnimationFrame:

    const queue = [];
    const mo = new MutationObserver(mutations => {
      if (!queue.length) requestAnimationFrame(process);
      queue.push(mutations);
    });
    function process() {
      for (const mutations of queue) {
        // ..........
      }
      queue.length = 0;
    }
    

回到问题:

观察一个非常确定的容器ul#my-list,看看是否有任何<li>附加到它上面。

由于li是直接子节点,并且我们寻找添加的节点,因此唯一需要的选项childList: true(参见上面的建议 #2)。

new MutationObserver(function(mutations, observer) {
    // Do something here

    // Stop observing if needed:
    observer.disconnect();
}).observe(document.querySelector('ul#my-list'), {childList: true});
于 2016-09-05T14:04:23.123 回答