Jason Pan

潘忠显 / 2021-04-10


“JavaScript 工作原理”系列文章是翻译和整理自 SessionStack 网站的 How JavaScript works。因为博文发表于2017年,部分技术或信息可能已经过时。本文英文原文链接,作者 Alexander Zlatkov,翻译 潘忠显

How JavaScript works: tracking changes in the DOM using MutationObserver

这是致力于探索JavaScript及其构建组件的系列文章的第10章。在确定和描述核心元素的过程中,我们还分享了一些在构建[SessionStack]时使用的经验法则(https://www.sessionstack.com/?utm_source=medium&utm_medium=blog&utm_content=javascript-series-push-notifications -intro),这是一个JavaScript应用程序,需要强大且高效能,以帮助用户实时查看和再现其Web应用程序缺陷。

This is post # 10 of the series dedicated to exploring JavaScript and its building components. In the process of identifying and describing the core elements, we also share some rules of thumb we use when building SessionStack, a JavaScript application that needs to be robust and highly-performant to help users see and reproduce their web app defects real-time.

如果错过了前几章,可以在这里找到它们:

img

由于许多原因,例如需要更丰富的UI来容纳更复杂的应用程序必须提供的功能,实时计算等,Web应用程序在客户端变得越来越繁琐。

增加的复杂性使得在Web应用程序生命周期中的每个给定时刻更难知道UI的确切状态。

例如,如果您要构建某种框架或仅库,而该库必须做出反应并执行依赖于DOM的某些操作,则这将变得更加困难。

Web apps are getting increasingly heavy on the client-side, due to many reasons such as the need of a richer UI to accommodate what more complex apps have to offer, real-time calculations, and so on.

The increased complexity makes it harder to know the exact state of the UI at every given moment during the lifecycle of your web app.

This gets even harder if you’re building some kind of a framework or just a library, for example, that has to react and perform certain actions that are dependent on the DOM.

Overview

MutationObserver is a Web API provided by modern browsers for detecting changes in the DOM. With this API one can listen to newly added or removed nodes, attribute changes or changes in the text content of text nodes.

Why would you want to do that?

There are quite a few cases in which the MutationObserver API can come really handy. For instance:

[MutationObserver](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver)是现代浏览器提供的Web API,用于检测DOM中的更改。使用此API,可以侦听新添加或删除的节点,属性更改或文本节点的文本内容更改。

你为什么想这么做?

在很多情况下,MutationObserver API确实非常有用。例如:

-您想通知您的Web应用程序访问者,他当前所在的页面已发生某些更改。 -您正在开发一个新的高级JavaScript框架,该框架会根据DOM的变化动态加载JavaScript模块。 -您可能正在使用所见即所得的编辑器,试图实现撤消/重做功能。通过使用MutationObserver API,您可以随时了解已进行的更改,因此可以轻松地撤消它们。

img

These are just a few examples of how the MutationObserver can be of help.

这些只是MutationObserver如何提供帮助的几个示例。

How to use MutationObserver

##如何使用MutationObserver

在您的应用程序中实现MutationObserver非常容易。您需要通过向其传递一个函数来创建MutationObserver实例,该实例每次发生突变时都会被调用。该函数的第一个参数是在单个批处理中发生的所有突变的集合。每个突变都提供有关其类型和已发生的变化的信息。

Implementing MutationObserver into your app is rather easy. You need to create a MutationObserver instance by passing it a function that would be called every time a mutation has occurred. The first argument of the function is a collection of all mutations which have occurred in a single batch. Each mutation provides information about its type and the changes which have occurred.

var mutationObserver = new MutationObserver(function(mutations) {
  mutations.forEach(function(mutation) {
    console.log(mutation);
  });
});

创建的对象具有三种方法:

-observe-开始倾听变化。接受两个参数-您要观察的DOM节点和一个设置对象 -disconnect-停止监听更改 -takeRecords-返回触发回调之前的最后一批更改。

以下代码段显示了如何开始观察:

The created object has three methods:

The following snippet shows how to start observing:

// Starts listening for changes in the root HTML element of the page.
mutationObserver.observe(document.documentElement, {
  attributes: true,
  characterData: true,
  childList: true,
  subtree: true,
  attributeOldValue: true,
  characterDataOldValue: true
});

现在,假设您在DOM中有一个非常简单的div

Now, let’s say that you have some very simple div in the DOM:

<div id="sample-div" class="test"> Simple div </div>

使用jQuery,您可以从该div中删除class属性:

Using jQuery, you canremove the class attribute from that div:

$("#sample-div").removeAttr("class");

当我们开始观察时,在调用mutationObserver.observe(...)之后,我们将在相应[MutationRecord](https://developer.mozilla.org/zh-CN/ docs / Web / API / MutationRecord):

As we have started observing, after calling mutationObserver.observe(...) we’re going to see a log in the console of the respective MutationRecord:

img

这是由于删除“ class”属性引起的变异。

最后,为了在作业完成后停止观察DOM,您可以执行以下操作:

This is the mutation caused by removing the class attribute.

And finally, in order to stop observing the DOM after the job is done, you can do the following:

// Stops the MutationObserver from listening for changes.
mutationObserver.disconnect();

如今,广泛支持MutationObserver

Nowadays, the MutationObserver is widely supported:

img

Alternatives

备择方案

但是,MutationObserver并不总是存在。那么,在MutationObserver出现之前,开发人员采取了什么措施?

The MutationObserver, however, has not always been around. So what did developers resort to before the MutationObserver came along?

还有其他一些可用选项:

-投票 -** MutationEvents ** -** CSS动画**

There are a few other options available:

Polling

##轮询

最简单,最简单的方法是通过轮询。使用浏览器setInterval WebAPI,您可以设置一个任务,该任务将定期检查是否发生了任何更改。自然,此方法会大大降低Web应用程序/网站的性能。

The simplest and most unsophisticated way was by polling. Using the browser setInterval WebAPI you can set up a task that would periodically check if any changes have occurred. Naturally, this method significantly degrades web app/website performance.

MutationEvents

在2000年,引入了[MutationEvents API](https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Mutation_events)。尽管有用,但对DOM中的每个更改都会触发突变事件,这又会导致性能问题。如今,不推荐使用“ MutationEvents” API,不久之后,现代浏览器将完全不再支持它。

这是浏览器对MutationEvents的支持:

In the year 2000, the MutationEvents API was introduced. Albeit useful, mutation events are fired on every single change in the DOM which again causes performance issues. Nowadays the MutationEvents API has been deprecated, and soon modern browsers will stop supporting it altogether.

This is the browser support for MutationEvents:

img

CSS动画

一种有些奇怪的替代方法是依靠[CSS动画](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Animations/Using_CSS_animations)。这听起来可能有些混乱。基本上,该想法是创建一个动画,一旦将元素添加到DOM,就会触发该动画。动画开始的那一刻,就会触发“ animationstart”事件:如果您向该事件附加了事件处理程序,则可以确切知道何时将元素添加到DOM。动画的执行时间应该太短,以至于用户几乎看不见它。

A somewhat strange alternative is one that relies on CSS animations. It might sound a bit confusing. Basically, the idea is to create an animation which would be triggered once an element has been added to the DOM. The moment the animation starts, the animationstart event will be fired: if you have attached an event handler to that event, you’d know exactly when the element has been added to the DOM. The animation’s execution time period should be so small that it’s practically invisible to the user.

首先,我们需要一个父元素,在其中我们要侦听节点插入:

First, we need a parent element, inside which, we’d like to listen to node insertions:

<div id=”container-element”></div>

In order to get a handle on node insertion, we need to set up a series of keyframe animations which will start when the node is inserted:

为了处理节点插入,我们需要设置一系列[keyframe](https://www.w3schools.com/cssref/css3_pr_animation-keyframes.asp)动画,这些动画将在插入节点时开始:

@keyframes nodeInserted { 
 from { opacity: 0.99; }
 to { opacity: 1; } 
}

创建关键帧后,需要将动画应用到您想听的元素上。请注意,持续时间很短,它们使浏览器中的动画显示变得轻松:

With the keyframes created, the animation needs to be applied on the elements you’d like to listen for. Note the small durations — they are relaxing the animation footprint in the browser:

#container-element * {
 animation-duration: 0.001s;
 animation-name: nodeInserted;
}

这会将动画添加到“ container-element”的所有子节点。动画结束时,将触发插入事件。

我们需要一个JavaScript函数来充当事件监听器。在该函数中,必须对“ event.animationName”进行初始检查,以确保它是我们想要的动画。

This adds the animation to all child nodes of the container-element. When the animation ends, the insertion event will fire.

We need a JavaScript function which will act as the event listener. Within the function, the initial event.animationName check must be made to ensure it’s the animation we want.

var insertionListener = function(event) {
  // Making sure that this is the animation we want.
  if (event.animationName === "nodeInserted") {
    console.log("Node has been inserted: " + event.target);
  }
}

Now it’s time to add the event listener to the parent:

现在是时候将事件侦听器添加到父级了:

document.addEventListener(animationstart, insertionListener, false); // standard + firefox
document.addEventListener(MSAnimationStart, insertionListener, false); // IE
document.addEventListener(webkitAnimationStart, insertionListener, false); // Chrome + Safari

这是浏览器对CSS动画的支持:

This is the browser support for CSS animations:

img

与上述解决方案相比,“ MutationObserver”具有许多优点。从本质上讲,它涵盖了DOM中可能发生的每一项更改,并且它可以批量触发更改,因此可以进行更好的优化。最重要的是,所有主要的现代浏览器都支持“ MutationObserver”,以及一些在后台使用“ MutationEvents”的polyfill。

MutationObserver offers a number of advantages over the above-mentioned solutions. In essence, it covers every single change that can possibly occur in the DOM and it’s way more optimized as it fires the changes in batches. On top of it, MutationObserver is supported by all major modern browsers, along with a couple of polyfills which use MutationEvents under the hood.

“ MutationObserver”在[SessionStack](https://www.sessionstack.com/?utm_source=medium&utm_medium=blog&utm_content=mutation-observer-post)的库中占据中心位置。

一旦将SessionStack的库集成到您的Web应用程序中,它将开始收集DOM更改,网络请求,异常,调试消息等数据,并将其发送到我们的服务器。SessionStack使用该数据来重新创建用户发生的所有事情。并以与您的用户相同的方式向您展示产品问题。不少用户认为SessionStack录制的是实际的视频,而实际上不是。录制实际的视频非常繁琐,而我们收集的少量数据却非常轻巧,不会影响您的网络应用的用户体验和性能。

MutationObserver occupies a central position in SessionStack’s library.

Once you integrate the SessionStack’s library in your web app, it starts collecting data such as DOM changes, network requests, exceptions, debug messages, etc. and sends it to our servers., SessionStack uses this data to recreate everything that happened to your users and show your product issues the same way they happened to your users. Quite a few users think that SessionStack records an actual video — it doesn’t. Recording an actual video is very heavy, while the small amount of data we gather is very lightweight and doesn’t impact the UX and performance of your web app.

Resources