例子里面的打印不是很有利于观察实际发生的情况,我稍微修改了一下,增加了每次 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)里描述了一些这个机制导致的问题。