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

Posted

技术标签:

【中文标题】有一个元素的引用,如何检测它何时附加到文档中?【英文标题】:Having a reference to an element, how to detect when it is appended to the document? 【发布时间】:2016-11-30 00:49:47 【问题描述】:

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

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

onceAppended(element, callback);

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

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

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

这是我的代码 sn-p:

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?

【问题讨论】:

@wOxxOm 如果您知道主题,请您完成我的功能吗?我尝试使用 MutationObservers 来实现这一点,但没有得到任何结果。 我不明白为什么 MutationObserver 不能处理这个任务。你试过什么?我想您必须将观察者附加到文档并检查每个添加的节点。不过,这将是非常无效的。所以也许你可以在 HTMLElement 和 Node 的原型中重写appendChildreplaceChild 等相关函数。 另见Alternative to DOMNodeInserted @wOxxOm 将观察者附加到文档是可能的解决方案之一。但是您可以想象它需要处理的事件数量。我正在寻找更好、对性能更友好的解决方案(如果有的话)。 @wOxxOm [UPD] 我相信,覆盖标准事件会产生副作用。 【参考方案1】:

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

相反,您必须将观察者放在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() 在添加任何元素时运行,只需第二次检查该元素是否存在于您的引用数组中。

【讨论】:

谢谢!这种方法足以解决任务,但它的最大缺点是性能。将数千个元素添加到页面(例如形成一个新表),回调会被调用数千次。【参考方案2】:

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

该解决方案在GitHub 和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 https://***.com/questions/38588741/having-a-reference-to-an-element-how-to-detect-once-it-appended-to-the-document (*** 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_APPENDEDfromto`;
        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);


方法2(使用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 https://***.com/questions/38588741/having-a-reference-to-an-element-how-to-detect-once-it-appended-to-the-document (*** 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);

【讨论】:

以上是关于有一个元素的引用,如何检测它何时附加到文档中?的主要内容,如果未能解决你的问题,请参考以下文章

Python Selenium StaleElementReferenceException:消息:过时的元素引用:元素未附加到页面文档

如何让 pcp 自动将节点附加到 postgres pgpool?

C ++:在向量中插入啥 - 指针或引用,向量何时复制它的元素?

如何检测何时需要保存 NSPersistentStore?

如何在Visual Studio代码中检测文档中语言的变化?

如何检测 App 何时移入/移出 Suspended 状态?