那些被忽略但很好用的 Web API / MutationObserver

我的改变,你看得见!

在开发网页过程中,我们最常做的事情就是对资料进行修改後运用在 DOM 元素上,像是新增 / 删除节点、调整样式、改写内容或是属性(attribute)的调整,而这些「修改 DOM」的动作通常散落在程序码的各个角落,发生时机也并不相同,在这样的情况下,我们能不能追踪这些动作呢?


MutationObserver

MutationObserver API 就是用来解决上述问题的,它可以让我们追踪 DOM 的变化,无论是子节点的变动或内容、属性的变动,并且获得相关的资讯,以便作出後续的行动。

# Window.MutationObserver

MutationObserver 本身是建构函式,所以我们需要用 new 关键字来建立实体,建立时需要传入一个 Callback Function 作为参数,该 Function 可以接到由 MutationObserver 提供的 MutationRecords 阵列和 MutationObserver 实体作为参数:

  • MutationRecords: 这个阵列会存放 MutationRecord 物件,该物件记录着 DOM 变动的相关资讯,後面再详细介绍。
  • MutationObserver: 呼叫此 Callback Function 的 MutationObserver 实体,其实就等於该 Function 的 this
const observer = new MutationObserver(function (mutations, owner) {
  console.log(mutation, owner);
});

 

# MutationObserver.observe

我们可以把刚刚 new 出来的 MutationObserver 实体譬喻成一个「观察者」,当这位观察者监测到节点变动,他就会执行设定好的任务(callback),但他目前并未被指派要观察哪一个元素,所以我们要透过 observe 这个 method 来指定「被观察的对象」,其中需要传入两个参数:

  • target: 一个要受到观察的 DOM 节点。
  • options:一个初始化设定物件,用来指定 DOM 节点的哪些项目需要被观察等相关设定。
属性 解释 型别、预设值
childList 是否观察节点的直属子节点变动 boolean、false
subtree 是否观察节点的所有子节点变动 boolean、false
attributes 是否观察节点的属性变动 boolean、false
characterData 是否观察节点中的内容变动 boolean、false
attributeOldValue 是否纪录变动前的属性值 boolean、false
characterDataOldValue 是否纪录变动前的内容值 boolean、false
attributeFilter 需要观察的属性名称,如果为空则全部观察 [string]、[]
const observer = new MutationObserver(function (mutations) {
  console.log(mutations);
});

const div = document.querySelector("div");
observer.observe(div, {
  childList: true,
  attributes: true,
  characterData: true,
});

如此一来,只要元素有被观察到我们所指定项目的变动,MutationObserver 就会去执行 Callback。

 

# MutationObserver.disconnect

另外还可以透过 disconnect 来注销目前已经被观察的 DOM,但 MutationObserver 实体并不会消失,只是暂时不再进行观察,直到你又使用 observe 来注册一个被观察对象。

const observer = new MutationObserver(function (mutations) {
  console.log(mutations);
});

observer.disconnect();

 

# MutationRecord 物件

前面有说,MutationObserver 在执行 Callback 时会提供一个 MutationRecords 阵列,里面会存放 MutationRecord 物件,这个段落我们要来了解为何 MutationRecord 要存放在阵列中,以及它到底存放着哪些资讯供我们使用。

  • 首先要先知道一个 MutationObserver 的特性:

    MutationObserver 并非事件监听,事件是同步执行的,而 MutationObserver 则是非同步执行的,这意味着,如果目前执行的一段程序中有多次的节点变动,MutationObserver 会等到一切结束後才呼叫 Callback。

    这是为了避免大量 DOM 操作所带来的效能问题,也因此,MutationObserver 会将该程序时间内所有的变动记录下来并包装成阵列给我们。

const observer = new MutationObserver(function (mutations) {
  mutations.forEach((record) => {
    console.log(record); // MutationRecord 物件
  });
});

const div = document.querySelector("div");
observer.observe(div, {
  childList: true,
  attributes: true,
  characterData: true,
});

div.textContent = "example";
div.style.background = "pink";
// 此时 MutationObserver 只会呼叫一次 Callback
// 而 mutations 中会有两个 MutationRecord
  • 接着来看一下 MutationRecord 中有哪些属性:
    以下这些并非全部的属性,只介绍了几个比较实用的,如果想知道完整内容的话可以看这里
属性 解释 型别
type 观察到的变动类型 string
target 变动的节点 Node
addedNodes 被新增的节点,如果没有会是 null Node
removedNodes 被删除的节点,如果没有会是 null Node
attributeName 观察到的变动属性之名称 string
oldValue 变动前的值 string

MutationRecord.oldValue 只有在 observe()options 有开启设定时才会有值。

 

# 使用情境

老实说,一定要使用 MutationObserver 的情况并不多,可能比较需要使用的情境会是:「专案中使用了第三方套件,为了观测该套件所进行的一些 DOM 操作」,由於我们无法直接对第三方套件的程序码进行修改,导致无法掌控 DOM 变动的程序,所以只能透过 MutationObserver 来追踪。

但因为平常的 DOM 操作其实都是由我们主动执行的,所以真的需要在节点变动後做什麽事情就直接放在後面一起执行就好了,例如:

<button onclick="changeContent()">改变内容</button>
<button onclick="changeColor()">改变颜色</button>
<div>Hi I'm Max</div>

<script>
  const div = document.querySelector("div");

  // 当 DOM 变动时我们想做的事情
  function onDomMutation(info) {
    console.log(info);
  }

  function changeContent() {
    const oldValue = div.textContent;
    div.textContent = "Hi I'm Tom";
    onDomMutation({
      target: div,
      oldValue,
      type: "characterData",
    });
  }

  function changeColor() {
    const oldValue = div.style;
    div.style.color = "red";
    onDomMutation({
      target: div,
      oldValue,
      type: "attributes",
    });
  }
</script>

其实透过上面这样的写法依然可以做到类似的效果,但我认为 MutationObserver 最主要的优点是「减少耦合」,就像之前介绍的 CustomEvent 一样,透过「观察者模式」的 Design Patterns 来让原本是许多「一对一的依赖关系」整合成单个「一对多的依赖关系」。

<button onclick="changeContent()">改变内容</button>
<button onclick="changeColor()">改变颜色</button>
<div>Hi I'm Max</div>

<script>
  const div = document.querySelector("div");
  const observer = new MutationObserver(function (mutations) {
    mutations.forEach((record) => {
      console.log(record);
    });
  });

  observer.observe(div, {
    childList: true,
    attributes: true,
    characterData: true,
  });

  function changeContent() {
    div.textContent = "Hi I'm Tom";
  }

  function changeColor() {
    div.style.color = "red";
  }
</script>

 

不晓得经过这样的解释後,各位有没有理解使用 MutationObserver 的好处呢?後面我们还会陆续介绍几个也是采用「观察者模式(Observer Pattern)」的 API,可以期待一下喔。另外有使用过 Vue 的朋友,你知道 $nextTick 其实就是基於 MutationObserver 实践出来的喔,有兴趣的人可以去看看原始码


<<:  Day 20 : Linux - 安装Linux的VM虚拟机part_2,如何自己手动分割硬碟?分割区跟挂载点又该如何做选择?

>>:  [第19天]理财达人Mx. Ada-Telegram Bot

Day17 React-Router(二)Route设置进阶

讲完最基础的Route设置之後, 来学习如何更准确的经由path来渲染画面上的元件。 Route标签...

Day 24 - 了解文字艺术

Rita.js Rita 使用范例 拆解字串:RiString() 取得词性 pos()(Part ...

[Day 20] 实作 - 介面篇4

接着在ActionBattle_SkillScene.js下继续写 定义一个类别为Window_Ke...

《你的地图会说话? WebGIS与JavaScript的情感交织》结束,才是真正的开始。

一路走来 不知不觉已到了Day30了,这一天说长不长说短不短。 其实大概从Day5开始,就已经觉得很...

Day 25 去识别化定义规划实作

今天就根据GDPR第4(5)条和 CCPA §§1798.140(o)、§§1798.145(c)-...