DOM – MutationObserver

介绍

它和 IntersectionObserver, ResizeObserver 差不多, 都是观察 element 变化的. 

它可以观察元素的 attribute 增加, 移除, 修改, append child 等等.

建议先看前 2 篇 IntersectionObserverResizeObserver 一起了解会比较容易.

 

new MutationObserver()

const mo = new MutationObserver((mutations) => {
  console.log("mutations", mutations);
});

mo.observe(document.querySelector(".container"), {
  attributes: true,
});

调用方式和 Intersection, ResizeObserver 是一样的.

注意: 它和 IO 和 Resize 有个区别,  IO, Resize 调用 observe 第一次会马上触发掉, 但 Mutation 没有, 它会等到真的有改变时才触发.

还有一个特别之处是 observe 的时候需要一个 config. 指定要观察的范围.

出于性能考虑, 观察的范围越广性能越伤, 所以请按需设置哦. 


观察 attributes

attributes: true

可以观察到元素添加, 移除, 修改 attribute. (注: 只要有 value set 即便 value 是一样的, 它依然会触发哦, 如果不想这样, 我们可以通过 oldValue 来 filter 掉。相关 Issue)

callback 获取的是 MutationRecord,而不是 Entry(IntersectionObserver 和 ResizeObserver 则是 Entry)

record 是 array,它的数量由 element 和变更的多少来决定。

比如说,observe 两个 element,同时间内(下面会讲解什么叫同时间内),两个 element 都变更了 attribute 那就是两个 records。

如果是 observe 一个 element,但同时间内,有两个 attrubute 变更,那也会拿到两个 record。

attributeOldValue: true

多了一个 oldValue。

提醒:如果没有 attribute,那 oldValue 会是 null,而不是 undefined 哦。

attributeFilter: ["contenteditable"]

指定要观察的 attribute, 没有在 list 里面的, 添加, 移除, 修改都不会触发.

 

观察 Child 和 Descendant Element

childList: true

当元素 appendChild / removeChild 的时候触发.

subtree: true

当元素有子孙后裔插入或移除时触发.

特别要留意的是 Record 的 target element。

通常这个 target element 是指 observe element,但是也有例外。

比如说

<div class="card">
  <div class="parent">
    <div class="child">
      <h1>Hello World 1</h1>
    </div>
  </div>
</div>

<div class="card">
  <div class="parent">
    <div class="child">
      <h1>Hello World 2</h1>
    </div>
  </div>
</div>

有两个 card,里面有 parent > child 两层

我们尝试往它们的 child append element

const cards = Array.from(document.querySelectorAll('.card'));
const card1 = cards[0];
const card2 = cards[1];

const mo = new MutationObserver(records => {
  console.log(records);
});

mo.observe(card1, { childList: true, subtree: true });
mo.observe(card2, { childList: true, subtree: true });

window.setTimeout(() => {
  for (const card of cards) {
    const p = document.createElement('p');
    p.textContent = 'test only';
    card.querySelector('.child')!.appendChild(p);
  }
}, 1000);

效果

image

 会得到两个 Record,target 指向的不是 .card,而是 .child element,因为 new element 是 append to .child。

 

观察 TextNode textcontent

characterData: true & characterDataOldValue: true

const textNode = document.createTextNode("Hello World");
mo.observe(textNode, { characterData: true, characterDataOldValue: true });
setTimeout(() => {
  textNode.textContent = "SuperMan";
}, 3000);

效果

注意, 一定要是 TextNode 哦.

const p = document.createElement("p");
p.textContent = "Hello World";
mo.observe(p, { characterData: true, characterDataOldValue: true });
setTimeout(() => {
  p.textContent = "SuperMan";
  document.body.append(p);
}, 3000);

换成 p 就不灵了.

虽然 p 是有效的, 可以 append to body 看到字, 但是 observer 没有监听到它. 所以监听的节点一定要是 TextNode.

简单说就是,想监听 p append 用 childList,想监听 p 里面的 TextNode 变更就用 characterData。

另外,subtree: true 也适用于 characterData。

 

触发时机

和 IntersectionObserver,ResizeObserver 不同。

MutationObserver 触发的很早,它类似 Microtask。

而 IntersectionObserver,ResizeObserver 则是 after ui render。

也合理啦,毕竟 MutationObserver callback 获取的资料比如 addedNodes, removedNode 这些都不需要等 ui render。

window.setTimeout(() => {
  const container = document.querySelector<HTMLElement>('.container')!;
  const mo = new MutationObserver(
    records => console.log(records[0].addedNodes.length > 0 ? 'mutation add' : 'mutation remove'), // 2, 4
  );
  mo.observe(container, { childList: true, subtree: true });

  requestAnimationFrame(() => console.log('rAF')); // 6

  const h1 = container.querySelector('h1')!;
  h1.remove();
  console.log('sync'); // 1

  queueMicrotask(() => {
    console.log('micro1');   // 3
    container.appendChild(h1);

    queueMicrotask(() => {
      console.log('micro2'); // 5
    });
  });
}, 2000);

效果

相当于,h1.remove 之后它就 queueMicrotask for mutation callback。

Multiple MutationObserver 之前的触发时机

有两个 MutationObserver 同时 observe 一个 element

const div = document.createElement('div');
const mo1 = new MutationObserver(() => {
  log('mo1');
  queueMicrotask(() => log('mic1'));
  window.requestAnimationFrame(() => log('raf1'));
});
const mo2 = new MutationObserver(() => log('mo2'));
mo1.observe(div, { attributes: true, attributeFilter: ['class'] });
mo2.observe(div, { attributes: true, attributeFilter: ['class'] });
div.classList.add('test');

// 触发顺序:mo1...mo2...mic1...raf1

当这个 element 变更时

  1.  哪一个 MutationObserver 先触发?

    答案是先创建实例的那一个先触发。

    不是看谁先 observe 哦,是看谁先实例化出 MutationObserver 对象。

  2. 它们触发之间的间隔又是多久?

    没有间隔。执行完 mo1 的 callback 会立刻会执行 mo2 callback。

    假如我们在 mo1 callback 里 queueMicrotask,它会在 mo2 callback 执行完了后才被执行。

 

没有 unobserve 方法,但有 takeRecords 方法

相关 Issue – Disconnect single target instead of all

不像 IntersectionObserver 和 ResizeObserver 有 unobserve 可以取消指定 element 的监听,

MutationObserver 只有 disconnect 一次性取消所有监听。

workaround 的方案是 wrap 一层,然后自己记入 element,unobserve 时,调用 disconnect 然后在 re-observe 其它的回去😅。参考高赞回复

另外 MutationObserver  有一个 takeRecords 方法,这个方法用在 disconnect 之前,因为 MutationObserver 触发是 microtask,

所以在同步执行过程中,如果你 disconnect,这时是有可能已经有一些 MutationRecord 即将要发布的,而这个 takeRecords 方法就可以把它们拿出来处理,然后你才 disconnect。

 

posted @ 2022-03-11 21:15  兴杰  阅读(216)  评论(0)    收藏  举报