引用了一个元素,如何检测它何时附加到文档中?

Having a reference to an element, how to detect when it is appended to the document?

我正在开发一个 JavaScript 模块,它对使用它的环境一无所知。

并且,从技术上讲,我想实现下一个功能:

onceAppended(element, callback);

element 是一个 HTMLElement 并且此元素的父元素在模块初始化期间可能是未知的。 callback是一个函数,当页面出现element时必须触发

如果将元素附加到文档,则必须立即调用回调。如果尚未附加 element,一旦 element 出现在文档中,函数将触发 callback

问题是,我们可以使用 DOMNodeInserted 变异事件来检测 element 追加事件。但是突变事件现在deprecated。而且好像MutationObserver也做不了这个任务吧?

这是我的代码片段:

function onceAppended (element, callback) {
    let el = element,
        listener;
    while (el.parentNode)
        el = el.parentNode;
    if (el instanceof Document) {
        callback();
        return;
    }
    if (typeof MutationObserver === "undefined") { // use deprecated method
        element.addEventListener("DOMNodeInserted", listener = (ev) => {
            if (ev.path.length > 1 && ev.path[ev.length - 2] instanceof Document) {
                element.removeEventListener("DOMNodeInserted", listener);
                callback();
            }
        }, false);
        return;
    }
    // Can't MutationObserver detect append event for the case?
}

不幸的是,无法以与 DOMNodeInserted 完全相同的方式执行此操作,因为 MutationObserver 事件的 none 会告诉您元素的 父元素何时更改.

相反,您必须将观察者放在 document.body 上并检查附加的每个节点。如果你想 运行 每当附加任何节点时你的回调,那很容易。如果您只希望它在附加某些节点时 运行,那么您必须在某处保留对这些节点的引用。

let elements = [];
elements[0] = document.createElement('div');
elements[1] = document.createElement('span');
elements[2] = document.createElement('p');
elements[3] = document.createElement('a');

const MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;

const observer = new MutationObserver(function(mutations) {
  // 'addedNodes' is an array of nodes that were appended to the DOM.
  // Checking its length let's us know if we've observed a node being added
  if (mutations[0].addedNodes.length > 0) {

    // 'indexOf' let's us know if the added node is in our reference array
    if (Array.prototype.indexOf.call(mutations[0].addedNodes[0], elements) > -1) {

      // Run the callback function with a reference to the element
      callback(mutations[0].addedNodes[0]);
    }
});

observer.observe(document.body, {
  childList: true,
  subtree: true
});

function callback(element) {
  console.log(element);
}

document.body.appendChild(elements[2]); // => '<p></p>'
elements[2].appendChild(elements[3]);   // => '<a></a>'

如您所见,回调是针对 document.body 内任何位置附加的节点触发的。如果您希望在任何元素被追加时 callback() 到 运行,只需进行第二次检查该元素是否存在于您的引用数组中。

通过 wOxxOm 关于 Alternative to DOMNodeInserted 的提示和 skyline3000 的回答,我开发了两种解决此任务的方法。第一种方法 onceAppended 很快,但在触发 callback 之前有大约 25ms 的延迟。第二种方法在插入元素后立即触发callback,但当应用程序中追加大量元素时可能会很慢。

解决方案在 GitHub and as an npm ES6 模块上可用。下面是两种方案的明码。

方法一(使用CSS动画)

function useDeprecatedMethod (element, callback) {
    let listener;
    return element.addEventListener(`DOMNodeInserted`, listener = (ev) => {
        if (ev.path.length > 1 && ev.path[ev.length - 2] instanceof Document) {
            element.removeEventListener(`DOMNodeInserted`, listener);
            callback();
        }
    }, false);
}

function isAppended (element) {
    while (element.parentNode)
        element = element.parentNode;
    return element instanceof Document;
}

/**
 * Method 1. Asynchronous. Has a better performance but also has an one-frame delay after element is
 * appended (around 25ms delay) of callback triggering.
 * This method is based on CSS3 animations and animationstart event handling.
 * Fires callback once element is appended to the document.
 * @author ZitRo (https://github.com/ZitRos)
 * @see  (Whosebug original question)
 * @see https://github.com/ZitRos/dom-onceAppended (Home repository)
 * @see https://www.npmjs.com/package/dom-once-appended (npm package)
 * @param {HTMLElement} element - Element to be appended
 * @param {function} callback - Append event handler
 */
export function onceAppended (element, callback) {

    if (isAppended(element)) {
        callback();
        return;
    }

    let sName = `animation`, pName = ``;

    if ( // since DOMNodeInserted event is deprecated, we will try to avoid using it
        typeof element.style[sName] === `undefined`
        && (sName = `webkitAnimation`) && (pName = "-webkit-")
            && typeof element.style[sName] === `undefined`
        && (sName = `mozAnimation`) && (pName = "-moz-")
            && typeof element.style[sName] === `undefined`
        && (sName = `oAnimation`) && (pName = "-o-")
            && typeof element.style[sName] === `undefined`
    ) {
        return useDeprecatedMethod(element, callback);
    }

    if (!document.__ONCE_APPENDED) {
        document.__ONCE_APPENDED = document.createElement('style');
        document.__ONCE_APPENDED.textContent = `@${ pName }keyframes ONCE_APPENDED{from{}to{}}`;
        document.head.appendChild(document.__ONCE_APPENDED);
    }

    let oldAnimation = element.style[sName];
    element.style[sName] = `ONCE_APPENDED`;
    element.addEventListener(`animationstart`, () => {
        element.style[sName] = oldAnimation;
        callback();
    }, true);

}

方法二(使用MutationObserver)

function useDeprecatedMethod (element, callback) {
    let listener;
    return element.addEventListener(`DOMNodeInserted`, listener = (ev) => {
        if (ev.path.length > 1 && ev.path[ev.length - 2] instanceof Document) {
            element.removeEventListener(`DOMNodeInserted`, listener);
            callback();
        }
    }, false);
}

function isAppended (element) {
    while (element.parentNode)
        element = element.parentNode;
    return element instanceof Document;
}

/**
 * Method 2. Synchronous. Has a lower performance for pages with a lot of elements being inserted,
 * but triggers callback immediately after element insert.
 * This method is based on MutationObserver.
 * Fires callback once element is appended to the document.
 * @author ZitRo (https://github.com/ZitRos)
 * @see  (Whosebug original question)
 * @see https://github.com/ZitRos/dom-onceAppended (Home repository)
 * @see https://www.npmjs.com/package/dom-once-appended (npm package)
 * @param {HTMLElement} element - Element to be appended
 * @param {function} callback - Append event handler
 */
export function onceAppendedSync (element, callback) {

    if (isAppended(element)) {
        callback();
        return;
    }

    const MutationObserver =
        window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver;

    if (!MutationObserver)
        return useDeprecatedMethod(element, callback);

    const observer = new MutationObserver((mutations) => {
        if (mutations[0].addedNodes.length === 0)
            return;
        if (Array.prototype.indexOf.call(mutations[0].addedNodes, element) === -1)
            return;
        observer.disconnect();
        callback();
    });

    observer.observe(document.body, {
        childList: true,
        subtree: true
    });

}

这两种方法的用法相同,只是函数名不同:

import { onceAppended } from "dom-once-appended"; // or onceAppendedSync

function myModule () {
    let sampleElement = document.createElement("div");
    onceAppended(sampleElement, () => { // or onceAppendedSync
        console.log(`Sample element is appended!`);
    });
    return sampleElement;
}

// somewhere else in the sources (example)
let element = myModule();
setTimeout(() => document.body.appendChild(element), 200);