估计看一眼手册知道有这么个东西,然后就再也不用,因为很少写原生 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') 一次都没有
为了移植一个脚本和这个东西纠缠一天,希望设计这个函数的祭天一下
1
autoxbc OP 好像没有人回复时,自己就不能插附言
|
2
binux 2019-01-04 04:11:22 +08:00 1
你应该 mttn.target.querySelector 而不是遍历 addedNodes 中的 node,否则你应该 node.tagName == 'P'。
|
3
autoxbc OP @binux #2 您说的对。我考虑 mttn.target.querySelector 会去 addedNodes 的兄弟节点查找,是个不必要的性能损失,某些情况会有误判
|
4
binux 2019-01-04 05:16:49 +08:00
@autoxbc #3 你要么取交,要么用类似 http://api.jquery.com/is/ 的东西
|
5
aryu 2019-01-04 09:53:14 +08:00 2
例子里面的打印不是很有利于观察实际发生的情况,我稍微修改了一下,增加了每次 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)里描述了一些这个机制导致的问题。 |
6
aryu 2019-01-04 09:57:11 +08:00
排版好像出了些问题,我放在一个 gist 里了
https://gist.github.com/Yuyz0112/005e11735056f0bc992ce821e48647e1 |
7
shidianxia 2019-01-04 10:25:54 +08:00
收藏了,感谢各位,学到了点新东西。
|