V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐关注
Meteor
JSLint - a JavaScript code quality tool
jsFiddle
D3.js
WebStorm
推荐书目
JavaScript 权威指南第 5 版
Closure: The Definitive Guide
autoxbc
V2EX  ›  JavaScript

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

  •  
  •   autoxbc · 2019-01-04 03:23:01 +08:00 · 4264 次点击
    这是一个创建于 2153 天前的主题,其中的信息可能已经有所发展或是发生改变。

    估计看一眼手册知道有这么个东西,然后就再也不用,因为很少写原生 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 条附言  ·  2019-01-04 04:11:00 +08:00
    第二条写错了,被自己误导了

    script 标签会导致 p 和 a 分入不同突变数组,但是 a 被捕获时没附带父节点不是 script 造成的。原因是 a 不是首节点,逐格捕获时和 p 行为不一致
    7 条回复    2019-01-04 10:25:54 +08:00
    autoxbc
        1
    autoxbc  
    OP
       2019-01-04 03:52:48 +08:00   ❤️ 1
    好像没有人回复时,自己就不能插附言
    binux
        2
    binux  
       2019-01-04 04:11:22 +08:00   ❤️ 1
    你应该 mttn.target.querySelector 而不是遍历 addedNodes 中的 node,否则你应该 node.tagName == 'P'。
    autoxbc
        3
    autoxbc  
    OP
       2019-01-04 04:57:29 +08:00
    @binux #2 您说的对。我考虑 mttn.target.querySelector 会去 addedNodes 的兄弟节点查找,是个不必要的性能损失,某些情况会有误判
    binux
        4
    binux  
       2019-01-04 05:16:49 +08:00
    @autoxbc #3 你要么取交,要么用类似 http://api.jquery.com/is/ 的东西
    aryu
        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)里描述了一些这个机制导致的问题。
    aryu
        6
    aryu  
       2019-01-04 09:57:11 +08:00
    排版好像出了些问题,我放在一个 gist 里了
    https://gist.github.com/Yuyz0112/005e11735056f0bc992ce821e48647e1
    shidianxia
        7
    shidianxia  
       2019-01-04 10:25:54 +08:00
    收藏了,感谢各位,学到了点新东西。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   5605 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 01:32 · PVG 09:32 · LAX 17:32 · JFK 20:32
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.