我猜很多人都没真的用过 MutationObserver

2019-01-04 03:23:01 +08:00
 autoxbc

估计看一眼手册知道有这么个东西,然后就再也不用,因为很少写原生 DOM 操作

找了很多帖子,都没有提到这个函数有两个巨坑,我来展示一下

<!DOCTYPE html>
  <head>
    <script>
      const callback = mttns => {
        mttns.forEach( mttn => {
          [...mttn.addedNodes].forEach( node => {
            if( node.nodeType !== 1 )
              return;
              
            if( node.querySelector('div p') )
              console.log('div p ' + Math.random() );
            if( node.querySelector('div a') )
              console.log('div a ' + Math.random() );
          } );
        } );
      };
      const opts = { childList:true , subtree:true };
      new MutationObserver(callback).observe( document , opts );
    </script>
  </head>
  <body>
    <div>
      <p></p>
      <script></script>
      <a></a>
    </div>
  </body>
</html>

不用真的运行,推测一下控制台里 div p 和 div a 会出现几次?没用过的话会回答各出现一次。答案是 div p 会出现两次,而 div a 一次都没有

从那些千篇一律的文章中是找不到原因的,他们照着手册写个 demo 就撤了

虽然 MutationObserver 打着替代 DOMNodeInserted,DOMContentLoaded 的旗号,其实工作过程并不一致。对于普通的节点插入,MutationObserver 捕获的节点和 DOMNodeInserted 一样,但是对于整个 html 文档的第一次载入,也就是对应 DOMContentLoaded,MutationObserver 展示出了诡异之处

1 . 例子中的 html 片段,在首次载入时,并不是直接捕获一个 html 节点,而是

捕获 body > div > p

捕获 div > p

捕获 p

你问我是怎么回事?答案是回音,案是回音,是回音,回音,音

MutationObserver 会乐此不彼的重复这个过程,所以 node.querySelector('div p') 出现两次。这个行为和普通的节点插入完全不同

2 . 和 DOMContentLoaded 给你一个 document(注意不是 document.documentElement) 不同,MutationObserver 似乎把整个文档当作流,逐格吐出结果,并且这个流会被打断,也就是 script 标签

实际是这样的过程

注意没有 body > div > a,只有

body > div > p

div > p

p

script:你好打断一下

a <- 没有爹的孩子

所以 node.querySelector('div a') 一次都没有


为了移植一个脚本和这个东西纠缠一天,希望设计这个函数的祭天一下

4290 次点击
所在节点    JavaScript
7 条回复
autoxbc
2019-01-04 03:52:48 +08:00
好像没有人回复时,自己就不能插附言
binux
2019-01-04 04:11:22 +08:00
你应该 mttn.target.querySelector 而不是遍历 addedNodes 中的 node,否则你应该 node.tagName == 'P'。
autoxbc
2019-01-04 04:57:29 +08:00
@binux #2 您说的对。我考虑 mttn.target.querySelector 会去 addedNodes 的兄弟节点查找,是个不必要的性能损失,某些情况会有误判
binux
2019-01-04 05:16:49 +08:00
@autoxbc #3 你要么取交,要么用类似 http://api.jquery.com/is/ 的东西
aryu
2019-01-04 09:53:14 +08:00
例子里面的打印不是很有利于观察实际发生的情况,我稍微修改了一下,增加了每次 callback 触发的打印和遍历 added node 时具体的 node 的打印。

```
<!DOCTYPE html>

<head>
<script>
const callback = mttns => {
console.log('callback here')
mttns.forEach(mttn => {
[...mttn.addedNodes].forEach(node => {
if (node.nodeType !== 1)
return;

console.log('added element type node', node)
if (node.querySelector('div p')) {
console.log('[found with selector "div p"]');
}
if (node.querySelector('div a')) {
console.log('[found with selector "div a"]');
}
});
});
};
const opts = { childList: true, subtree: true };
new MutationObserver(callback).observe(document, opts);
</script>
</head>

<body>
<div>
<p></p>
<script></script>
<a></a>
</div>
</body>

</html>
```

MutationObserver 的核心机制是“异步 + 批量”,主要是处于性能考虑,不过在使用上是容易造成一些误解。改动后的代码打印结果如下:

```
>>> callback here
added element type node <body>​…​</body>​
[found with selector "div p"]
added element type node <div>​…​</div>​
[found with selector "div p"]
added element type node <p>​</p>​
added element type node <script>​</script>​
>>> callback here
added element type node <a>​</a>​
```

可以看到以下现象:
1. 总共产生了两次 callback
2. 第一次 callback 添加了 body, div, p, script 四个 element node
3. 第二次 callback 添加了 a 这一个 element node
4. 如果你调整 script 标签的位置,你会发现 script 结束会立即触发一次 callback,这可能和 MutationObserver 具体的实现规则有关。

所以实际发生的情况是当第一次 callback 触发时,由于是异步的,所以第一次 callback 内包含的 4 个新增 node 已经存在于 DOM 中,query 'div p' 是可以查询到的,但是 a node 还没有插入,所以此时查询不到。
第二次 callback 时 a node 单独被插入,但是也不符合 'div p' 和 'div a' 的 query 规则,所以不触发打印。

MutationObserver 的异步批量回调机制确实需要比较细致的处理,最近开源的一个项目里也在一段[设计文档]( https://github.com/rrweb-io/rrweb/blob/master/docs/observer.zh_CN.md#%E6%96%B0%E5%A2%9E%E8%8A%82%E7%82%B9)里描述了一些这个机制导致的问题。
aryu
2019-01-04 09:57:11 +08:00
排版好像出了些问题,我放在一个 gist 里了
https://gist.github.com/Yuyz0112/005e11735056f0bc992ce821e48647e1
shidianxia
2019-01-04 10:25:54 +08:00
收藏了,感谢各位,学到了点新东西。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/523683

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX