关于javascript:处理尚不存在的-DOM-节点

50次阅读

共计 3742 个字符,预计需要花费 10 分钟才能阅读完成。

摸索 MutationObserver API 与传统轮询期待最终被创立的节点办法相比的优劣。

有时候,您须要操作尚未存在的 DOM 的某个局部。

呈现这种需要的起因有很多,但你最常看到的是在解决第三方脚本时,这些脚本会异步地将标记注入页面。举个例子,我最近须要在用户敞开 Google reCAPTCHA 的挑战时更新 UI。诸如 blur 事件的响应并没有失去工具的正式反对,所以我打算本人来设计一个事件监听器。然而,通过像 .querySelector() 这样的办法来尝试拜访节点会返回null,因为此时节点还没有被浏览器渲染,并且我也不晓得到底什么时候会被渲染。

为了更深刻地探讨这个问题,我设计了一个按钮,让它在随机的工夫内(0 到 5 秒之间)被挂载到 DOM 中。如果我试图从一开始就给这个按钮增加一个事件监听器,我就会失去一个异样。

// Simulating lazily-rendered HTML:
setTimeout(() => {const button = document.createElement('button');
    button.id = 'button';
    button.innerText = 'Do Something!';

     document.body.append(button);
}, randomBetweenMs(1000, 5000));

document.querySelector('#button').addEventListener('click', () => {alert('clicked!')
});

// Error: Cannot read properties of null (reading 'addEventListener')

真的是毫无意外。你看到的所有代码都会被丢进调用栈并立刻执行(当然,除了 setTimeout 的回调函数),所以当我试图拜访按钮时,我所失去的便是null

轮询

为了解决这个问题,通常做法是应用轮询,不停的查问 DOM 直到节点呈现。你可能会看到应用 setInterval 或者 setTimeout 这样的办法,上面是应用递归的例子:

function attachListenerToButton() {let button = document.getElementById('button');

  if (button) {button.addEventListener('click', () => alert('clicked!'));
    return;
  }

    // If the node doesn't exist yet, try
    // again on the next turn of the event loop.
  setTimeout(attachListenerToButton);
}

attachListenerToButton();

或者,你可能曾经见过一种基于 Promise 的办法,这感觉更古代一些:

async function attachListenerToButton() {let button = document.getElementById('button');

  while (!button) {
        // If the node doesn't exist yet, try
        // again on the next turn of the event loop.
    button = document.getElementById('button');
    await new Promise((resolve) => setTimeout(resolve));
  }

  button.addEventListener('click', () => alert('clicked!'));
}

attachListenerToButton();

不管怎么说,这种策略都有非同小可的代价 – 次要是性能。在这两个版本中,移除 setTimeout() 会导致脚本齐全同步运行,阻塞主线程,以及其余须要在主线程上进行的工作。没有输出事件会被解决。你的标签会被解冻。凌乱不会随之而来。

在这里插入一个 setTimeout()(或者setInterval),将下一次尝试推延到到事件循环的下一个迭代中,这样就能够在这期间执行其余工作。 但你依然在反复地占用调用栈,期待你的节点呈现。如果你想让你的代码很好地治理事件循环,那这就太不现实了。

你能够通过减少查问的间隔时间(比方每 200ms 查问一次)来缩小调用栈的收缩。然而你会面临这样的危险,即在节点呈现和你的工作执行之间产生了意想不到的事件。例如,如果你正在增加一个 click 事件监听器,你不心愿用户在几毫秒后才附加监听器之前就有机会点击该元素。这样的问题可能很少见,但当你稍后调试可能出错的代码时,它们必定会带来懊恼。

MutationObserver()

MutationObserver API 曾经存在一段时间了,在古代浏览器中失去了广泛支持。它的作用很简略:当 DOM 树发生变化(包含插入节点时)时执行某些操作。然而作为原生浏览器 API,你不须要像轮询一样思考性能问题。察看 body 外部任何变动的根本设置如下所示:

const domObserver = new MutationObserver((mutationList) => {// document.body has changed! Do something.});

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

对于咱们结构的示例,进一步欠缺也相当简略。每当树发生变化时,咱们将查问特定的节点。如果节点存在,则附加监听器。

const domObserver = new MutationObserver(() => {const button = document.getElementById('button');

  if (button) {button.addEventListener('click', () => alert('clicked!'));
  }
});

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

咱们传递给 .observe() 的选项很重要。将 childList 设置为 true 使观察器监督咱们所针对的节点(document.body)的变动,而 subtree:true 将导致监督其所有后辈。诚然,这里的 API 对我来说不是非常容易了解,因而在应用它满足本人的需要之前,值得破费一些工夫认真思考。

无论如何,这种特定的配置最实用于你不晓得节点可能被注入到何处的状况。然而,如果你确信它会呈现在某个元素中,那么更理智的做法是更加准确地定位指标。

清理

如果咱们将观察器保留为原样,每次 DOM 的变动都会有增加另一个点击事件监听器到同一个按钮的危险。你能够通过将点击事件回调拉到 MutationObserver 的回调之外的本人的变量中来解决这个问题(.addEventListener() 不会向具备雷同回调援用的节点增加监听器),但在不再须要它时即时清理观察器会更加直观。观察器上有一个很好的办法能够做到这一点:

const domObserver = new MutationObserver((_mutationList, observer) => {const button = document.getElementById('button');

    if (button) {button.addEventListener('click', () => console.log('clicked!'));

        // No need to observe anymore. Clean up!
        observer.disconnect();}
});

响应速度

我之前提到了轮询可能会在响应 DOM 更改时引入大量的假死工夫。很多危险取决于你应用的工夫距离大小,但 setTimeout()setInterval() 都在主工作队列上运行它们的回调,这意味着它们总是在事件循环的下一次迭代中运行。

然而,MutationObserver 在微工作队列上触发其回调,这意味着它不须要期待事件循环的残缺旋转就能够触发回调。它的响应性更高。

我在浏览器中应用 performance.now() 进行了一项根底试验,以查看将点击事件监听器增加到按钮上须要多长时间,此时它已挂载到 DOM 中。请记住,这是在咱们的 setTimeout() 中没有设置提早的状况下进行的,因而咱们看到的提早可能是事件循环自身的速度(加上其余因素)。以下是后果:

办法 增加监听器的提早
轮询 ~8ms
MutationObserver() ~.09ms

这是一个十分惊人的差别。应用轮询和零提早的 setTimeout() 来附加监听器的速度,大概比 MutationObserver 慢了 88 倍。这成果还不错。

总结

思考到性能劣势、更简略的 API 和广泛的浏览器反对,与 MutationObserver 相比,应用 DOM 轮询难以获得劣势。我心愿你在解决本人我的项目中的提早挂载节点时会发现它很有用。我本人也会寻找其余场景,在这些场景下,MutationObserver 可能也很有用。

以上就是本文的全部内容,如果对你有所帮忙,欢送珍藏、点赞、转发~

正文完
 0