使用 MutationObserver 检测何时将节点添加到文档

Using MutationObserver to detect when a Node is added to document

我想在创建特定 DOMNode 的上下文中检测何时将特定 DOMNode 添加到文档中。

这是我目前的情况:

function getThingThatFormatsItself() {
    const MY_NODE = createNode();

    const observer = new MutationObserver(function (records) {
        records.forEach(record => {
            record.addedNodes.forEach(n => {
                if (n === MY_NODE) {
                    observer.disconnect();
                    doFormatting();
                }
            });
        })
    });
    observer.observe(document, {childList: true, subtree: true});

    // do formatting stuff that relies on the element being in DOM
    function doFormatting() {
        console.log(`It's been added.`);
    }

    return MY_NODE;
}

/* ELSEWHERE IN MY CODE */

// Now that Thing is added to DOM, it can format itself.
// However, this doesn't work unless it's added via setTimeout!
// Also, in the meantime, we are needlessly iterating through every single node added to the document.
$("#Foo").append(getThingThatFormatsItself());    

两个问题:

  1. 除非在将 thingNode 添加到文档之前有一个 setTimeout,否则这不起作用。看来 .observe() 没有立即生效。这是真的吗?
  2. 在创建事物和添加到文档之间必须遍历添加到文档的每个节点是绝对荒谬的。

有没有一种方法可以查看何时添加我的节点,而不必依赖使用 setTimeout 的外部调用者,也不必同时遍历每个添加的节点?[​​=33=]

真的"confusing",说得好听一点,我无法观察到实际节点本身的添加和删除——只能观察到它的子节点。一些设计。还蛮"confusing"的是.observe()好像是放到了事件队列中,而不是立即执行。

  1. MutationObserver 回调 运行s 在微任务队列处理阶段之后 主代码阶段发生的事件循环周期结束时已完成,这就是为什么在当前 运行ning 代码完成后调用 doFormatting() 的原因(可以说是整个函数调用堆栈)。

    除非您的其他代码中有其他内容假设在当前事件循环中调用 doFormatting 或取决于正在更新的 DOM 布局,否则它应该或多或少与使用 setTimeout 相同在下一个事件循环周期回调到 运行。

    MutationObserver 累积批量突变并在微任务队列中报告它们的原因是为了提供比已弃用的同步 DOM 突变事件更快的观察能力。

    解决方案 1:在 doFormatting()

    之后对 运行 代码使用回调
    function onNodeAdopted(node, callback) {
      new MutationObserver((mutations, observer) => {
        if (node.parentNode) {
          observer.disconnect();
          callback(node);
        }
      }).observe(document, {childList: true, subtree: true});
      return node;
    }
    
    function getThingThatFormatsItself(callback) {
      return onNodeAdopted(createNode(), node => {
        doFormatting(node);
        console.log('Formatted');
        callback(node);
      });
    }
    
    $("#Foo").append(getThingThatFormatsItself(node => {
      console.log('This runs after doFormatting()'); 
      doMoreThings();
    }));
    console.log('This runs BEFORE doFormatting() as MutationObserver is asynchronous')
    

    解决方案 2:不要使用 MutationObserver,而是拦截 Node.prototype.appendChild:

    const formatOnAppend = (() => {
      const hooks = new Map();
      let appendChild;
      function appendChildHook(node) {
        appendChild.call(this, node);
        const fn = hooks.get(node);
        if (fn) {
          hooks.delete(node);
          // only restore if no one chained later
          if (!hooks.size && Node.prototype.appendChild === appendChildHook) {
            Node.prototype.appendChild = appendChild;
          }
          fn(node);
        }
        return node;
      } 
      return {
        register(node, callback) {
          if (!hooks.size) {
            appendChild = Node.prototype.appendChild;
            Node.prototype.appendChild = appendChildHook;
          }
          hooks.set(node, callback);
          return node;
        },
      }
    })();
    

    用法:

    function getThingThatFormatsItself() {
      return formatOnAppend.register(createNode(), node => {
        console.log('%o is added', node);
      });
    }
    

    其他要尝试的事情:window.queueMicrotask(callback) 代替 setTimeout 将一些相关代码排入微任务队列中。对于旧版浏览器,文章中提供了一个简单的 polyfill。

  2. 检查 document.contains(MY_NODE)(如果在 ShadowDOM 内则无济于事)或 MY_NODE.parentNode 而不是枚举突变:

    new MutationObserver((mutations, observer) => {
      if (MY_NODE.parentNode) {
        observer.disconnect();
        doFormatting();
      }
    }).observe(document, {childList: true, subtree: true});
    

    这也更可靠,因为在一般情况下,节点可能是另一个添加节点的子节点,而不是作为 addedNodes 数组中的单独项。