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

JS_*_*ler 5 javascript dom mutation-observers

我想在创建特定 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());    
Run Code Online (Sandbox Code Playgroud)

这有两个问题:

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

有没有办法查看我的节点何时添加,而不必依赖使用 setTimeout 的外部调用者,并且不必同时遍历每个添加的节点?

说得好听一点,我无法观察实际节点本身的添加和删除,这真的很“令人困惑”——只能观察它的子节点。一些设计。这也相当“令人困惑”,.observe()似乎是放在事件队列中而不是立即执行。

wOx*_*xOm 8

  1. MutationObserver 回调在微任务队列处理阶段的事件循环结束时运行,该阶段发生在主代码阶段完成之后,这就是为什么在当前运行的代码完成后调用 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')
    
    Run Code Online (Sandbox Code Playgroud)

    解决方案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;
        },
      }
    })();
    
    Run Code Online (Sandbox Code Playgroud)

    用法:

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

    其他可以尝试的事情:window.queueMicrotask(callback)而不是 setTimeout 将一些依赖代码排入微任务队列中。对于较旧的浏览器,本文中有一个简单的填充代码。

  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});
    
    Run Code Online (Sandbox Code Playgroud)

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