浏览器知识点整理(十四)浏览器是怎么监听 DOM 的变化的?

本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!html

前言

浏览器经过事件循环机制使页面“活”起来,在事件循环中宏任务和微任务有不一样的执行时机,而浏览器基于微任务的技术有 MutationObserverPromise 以及以 Promise 为基础开发出来的不少其余的技术。前端

前面的文章也花了很大的篇幅介绍 Promise,那么这篇文章就带你们了解一下 MutationObserver 这个微任务是什么?用来作什么的吧!vue

MutationObserver 是用来监听 DOM 变化的一套方法,而监听 DOM 变化一直是前端工程师一项很是核心的需求。好比不少 Web 应用都利用 HTML 与 JavaScript 构建其自定义控件,与一些内置控件不一样,这些控件不是固有的。为了与内置控件一块儿良好地工做,这些控件必须可以适应内容更改、响应事件和用户交互。所以,Web 应用须要监视 DOM 变化并及时地作出响应react

MutationObserver 是如今监听 DOM 变化的方法,那么在开始的时候是怎么监听的呢,了解监听 DOM 方法的演变有助于咱们更加深刻地理解浏览器是怎样运行的。git

早期轮询检测

在早期,浏览器并无提供对监听 DOM 的支持,因此那个时候要观察 DOM 是否变化,惟一能作的即是 轮询检测,好比使用 setTimeout 或者 setInterval 来定时检测 DOM 是否有改变。github

这种方式简单粗暴,可是会遇到两个问题:web

  • 若是时间间隔设置过长,DOM 变化 响应不够及时
  • 反过来若是时间间隔设置太短,又会 浪费不少无用的工做量 去检查 DOM,会让页面变得低效。

Mutation Event

在 2000 年的时候引入了 Mutation Event,它是在 DOM3 中定义的用于监听 DOM 树结构变化的事件,不过因为该事件存在兼容性以及性能上的问题已经被弃用。后端

Mutation Event 总共有7种事件:DOMNodeInsertedDOMNodeRemovedDOMSubtreeModifiedDOMAttrModifiedDOMCharacterDataModifiedDOMNodeInsertedIntoDocumentDOMNodeRemovedFromDocument设计模式

简单用法以下:数组

let box = document.getElementById('box')
box.addEventListener("DOMSubtreeModified", function () {
  console.log('box 元素被修改');
}, false);
复制代码

Mutation Event 采用了 观察者的设计模式,当 DOM 有变更时就会马上触发相应的事件,这种方式属于 同步回调

采用 Mutation Event 解决了 实时性 的问题,由于 DOM 一旦发生变化,就会当即调用 JavaScript 接口。可是 这种实时性形成了严重的性能问题,由于每次 DOM 变更,渲染引擎都会去调用 JS,这样会产生较大的性能开销。

好比利用 JS 动态建立或动态修改 50 个节点内容,就会触发 50 次回调,并且每一个回调函数都须要必定的执行时间,这里咱们假设每次回调的执行时间是 4ms ,那么 50 次回调的执行时间就是 200ms,若此时浏览器正在执行一个动画效果,因为 Mutation Event 触发回调事件,就会致使动画的卡顿。

也正是由于使用 Mutation Event 会致使页面性能问题,因此 Mutation Event 被反对使用,并逐步从 Web 标准事件中删除了。

MutationObserver

MutationObserver API 能够用来监视 DOM 的变化,包括属性的变化、节点的增减、内容的变化等。

MutationObserver 的使用

参考 MutationObserver 的 MDN 官方文档资料

MutationObserver 是一个构造器,用来实例化一个 Mutation 观察者对象,参数是一个回调函数,这个回调函数会在指定的 DOM 节点发送变化后执行,回调函数有两个参数:

  • mutations:节点变化记录数组(MutationRecord
  • observer:观察者对象自己
let observe = new MutationObserver(function (mutations, observer) {});
复制代码

MutationObserver 实例对象有三个方法,以下:

  • observe:配置 MutationObserver 在 DOM 更改匹配给定选项时,经过其回调函数开始接收通知。即设置观察目标,接受两个参数:
    • target:观察目标;
    • options:经过对象成员来设置观察选项
  • disconnect:阻止 MutationObserver 实例继续接收的通知,直到再次调用其 observe() 方法,该观察者对象包含的回调函数都不会再被调用。
  • takeRecords:从 MutationObserver 的通知队列中删除全部待处理的通知,并将它们返回到MutationRecord 对象的新 Array 中。即清空记录队列并返回里面的内容。

使用实例:

// 选择须要观察变更的节点
const targetNode = document.getElementById('box');
// 观察器的配置(须要观察什么变更)
const config = {
  attributes: true,
  childList: true,
  subtree: true
};
// 当观察到变更时执行的回调函数
const callback = function (mutationsList, observer) {
  for (let mutation of mutationsList) {
    if (mutation.type === 'childList') {
      console.log('有节点发生改变,当前节点的内容是:' + mutation.target.innerHTML);
    } else if (mutation.type === 'attributes') {
      console.log('修改了' + mutation.attributeName + '属性');
    }
  }
};
// 建立一个观察器实例并传入回调函数
const observer = new MutationObserver(callback);
// 以上述配置开始观察目标节点
observer.observe(targetNode, config);
// 以后,可中止观察
// observer.disconnect();
复制代码

MutationObserver 的改进优化

  • 首先,MutationObserver 将响应函数改为异步调用,能够不用在每次 DOM 变化都触发异步调用,而是等屡次 DOM 变化后,一次触发异步调用,而且还会使用一个数据结构来记录这期间全部的 DOM 变化。这样即便频繁地操纵 DOM,也不会对性能形成太大的影响。
  • 在每次 DOM 节点发生变化的时候,渲染引擎将变化记录封装成微任务,并将微任务添加进当前的微任务队列中。这样当执行到检查点的时候,V8 引擎就会按照顺序执行微任务。

综上所述,MutationObserver 采用了 异步 + 微任务 的策略来实现监听 DOM 的变化。

  • 经过异步操做解决了同步操做的性能问题
  • 经过微任务解决了实时性的问题

MutationObserver 和 Vue 中的 nextTick

Vue 中 nextTick 可让咱们在下次 DOM 更新循环结束以后执行延迟回调,用于得到更新后的 DOM。

那在 Vue 中是怎么实现 nextTick 的呢?

Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的全部数据变动。若是同一个 watcher 被屡次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免没必要要的计算和 DOM 操做是很是重要的。而后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工做。

而异步回调咱们知道有宏任务(macrotasks)和微任务(microtasks)两种,那为了让 nextTick 更快的执行,那确定是优先选择微任务(microtasks)的。要建立一个新的微任务(microtask),会优先使用 Promise,若是浏览器不支持,再尝试 MutationObserver。实在不支持,就只能用 setTimeout 这个宏任务了。

Vue 中的异步更新队列 是这样说的:

image.png

至于 MutationObserver 是怎么模拟 nextTick 的,能够看 源码,其实就是建立一个 TextNode 并监听内容变化,而后要 nextTick 的时候去改一下这个节点的文本内容:

var counter = 1
    var observer = new MutationObserver(nextTickHandler)
    var textNode = document.createTextNode(String(counter))
    observer.observe(textNode, {
      characterData: true
    })
    timerFunc = () => {
      counter = (counter + 1) % 2
      textNode.data = String(counter)
    }
复制代码

总结

这篇文章介绍了监听 DOM 变化技术方案的演化史,从轮询到 Mutation Event 再到最新使用的 MutationObserverMutationObserver 方案的核心就是采用微任务机制,有效地权衡了实时性和执行效率的问题

最后还简单介绍了 MutationObserver 和 Vue 中 nextTick 的关系。

相关文章
相关标签/搜索