JavaScript中的几个Observer

  1. IntersectionObserver
  2. MutationObserver
  3. ResizeObserver
  4. PerformanceObserver

IntersectionObserver

  1. 如果需要监听某个元素,当它在视口中可见的时候希望可以得到通知,这个 api 就是最佳选择,以往我们的做法是绑定容器的 scroll 事件,或者设定时器不停的调用 getBoundingClientRect() 获取元素位置,这样做的性能会很差,因为每次获取元素的位置都会引起整个布局的重新计算,如果一些元素被放到 iframe 里面,想要知道它们何时出现几乎是不可能的
  2. IntersectionObserver 提供了一种异步观察目标元素与其祖先元素或顶级文档视口(viewport)交叉状态的方法,其祖先元素或视口被称为根
  3. 当一个 IntersectionObserver 对象被创建时,其被配置为监听根中一段给定比例的可见区域。一旦 IntersectionObserver 被创建,则无法更改其配置,所以一个给定的观察者对象只能用来监听可见区域的特定变化值;然而,你可以在同一个观察者对象中配置监听多个目标元素
  4. 构造函数 IntersectionObserver(callback[, options])
    • callback 是一个回调函数,当其监听到目标元素的可见部分(的比例)超过了一个或多个阈值(threshold)时,会执行指定的回调函数
      • 回调函数接收两个参数
        • 接收监听元素的实时数据组成的数组 [IntersectionObserverEntry],每个被触发的阈值,都或多或少与指定阈值有偏差
        • 被调用的 intersectionObserver 实例
    • 每个 IntersectionObserverEntry 里包含如下属性
      • time 时间戳,可见性发生变化的时间,是一个高精度时间戳,单位为毫秒
      • rootBounds 根元素的位置信息
      • boundingClientRect 目标元素的位置信息
      • intersectionRect 交叉部分的位置信息
      • intersectionRatio 目标元素的可见比例,即 intersectionRectboundingClientRect 的比例,完全可见时为 1,完全不可见时小于等于 0
      • taget 目标元素,也叫被观察的元素,是一个 dom 节点对象
      • isVisible 未查阅相关资料,且不会发生变化
      • isIntersecting 目标元素是否与视口交叉。mdn 介绍:返回一个布尔值,如果目标元素与交叉区域观察者对象 (intersection observer) 的根相交,则返回 true .如果返回 true, 则 IntersectionObserverEntry 描述了变换到交叉时的状态; 如果返回 false, 那么可以由此判断,变换是从交叉状态到非交叉状态
    • options 是一些配置, [, options] 表示可选参数
      • root 目标元素的祖先元素,即该元素必须是目标元素的直接或间接父级,如果构造函数未传人 root 或其值是 null,则默认使用顶级文档作为视口
      • rootMargin 一个在计算交叉值时添加至 root 边界盒中的一组偏移量,写法类似 cssmarginmdn 介绍计算交叉时添加到根边界盒 (en-US) 的矩形偏移量,可以有效的缩小或扩大根的判定范围从而满足计算需要。此属性返回的值可能与调用构造函数时指定的值不同,因此可能需要更改该值,以匹配内部要求。所有的偏移量均可用像素(px)或百分比(%)来表达,默认值为 “0px 0px 0px 0px”
      • threshold 规定了一个监听目标与边界盒交叉区域的比例值,可以是一个具体的数值或是一组 0.01.0 之间的数组,mdn 介绍:一个包含阈值的列表,按升序排列,列表中的每个阈值都是监听对象的交叉区域与边界区域的比率。当监听对象的任何阈值被越过时,都会生成一个通知(Notification)。如果构造器未传入值,则默认值为 0
        5.实例方法和属性
    • rootoptions 只读
    • rootMarginoptions 只读
    • thresholdoptions 只读
    • disconnect() 停止监听目标
    • observe() 开始监听目标元素
    • takeRecords() 返回所有观察目标的 intersectionObserverEntry 对象数组
    • unobserve() 停止监听特定的目标元素,需要传人目标元素
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 实例化一个观察器
let observer = new IntersectionObserver(function(entries, observer) {
console.log(entries);
console.log(observer);
});

let target = document.querySelect('#app');
// 开始监听元素
observer.observe(target);

// 停止对目标元素的监听
observer.unobserve(target);

// 终止对所有目标的监听
observer.disconnect();
intersectionRatio
intersectionRatio
  1. 示例
  1. 图片懒加载例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const imgs = document.querySelectorAll('img[data-src]');
const config = {
rootMargin: '0px',
threshold: 0
};

let observer = new IntersectionObserver((entries, self) => {
entries.forEach(entry => {
if(entry.isIntersecting){
let img = entry.target;
let src = img.dataset.src;

if (src) {
img.src = src;
img.removeAttribute('data-src');
}

// 解除观察
self.unobserve(entry.target);
}
}, config)
});

imgs.forEach(img => observer.observe(img));
  1. 无限滚动例子 infinite scroll
1
2
3
4
5
6
7
8
9
10
let intersectionObserver = new IntersectionObserver(function(entries) {
// 如果不可见,就返回
if(entries[0].intersectionRatio <= 0) return;

loadItems(10); // 加载数据 伪代码

console.log('loaded new items');
});

intersectionObserver.observe($0);

MutationObserver

  1. 如果想要知道某个元素在某个时候发生了具体的哪些变化,MutationObserver 就是最佳选择
  2. 构造函数 MutationObserver(callback)
    • callback 一个回调函数,每当指定节点或子树以及配置项有 dom 变动时会被调用,回调函数拥有两个参数
      • 一个是描述了所有被触发改动 MutationRecord 对象数组
      • 另一个是调用该函数的 MutationObserver 对象
    • 返回值,一个新的、包含监听 DOM 变化回调函数的 MutationObserver 对象
  3. MutationObserver 对象的方法
    • disconnect() 阻止 MutationObserver 实例继续接收通知,直到再次调用其 observe() 方法,该观察者对象包含的回调函数都不会再被调用
    • observe() 配置 MutationObserverDOM 更改匹配给定选项时,通过其回调函数开始接收通知,就是说调用 observe 方法后开始观察 DOM,当观察者发现匹配观察请求中指定的配置项的更改后,callback() 方法开始调用
    • takeRecords()MutationObserver 的通知队列中删除所有待处理的通知,并将它们返回到 MutationRecord 对象的新 Array 中,就是返回已检测到但尚未由观察者的回调函数处理的所有匹配 DOM 更改的列表,使变更队列保持为空。此方法最常见的使用场景是在断开观察者之前立即获取所有未处理的更改记录,以便在停止观察者时可以处理任何未处理的更改
  4. MutationRecord
    • 每个 MutationRecord 都代表一个独立的 DOM 变化,在每次随 DOM 变化调用 MutationObserver 的回调函数时,一个相应的 MutationRecord 会被作为参数,传递给回调函数
    • 属性
      • MutationRecord.type,类型是 string,如果是属性变化,则返回 "attributes" 如果是 characterData 节点变化,则返回 "characterData" 如果是子节点树 childList 变化,则返回 "childList"
      • MutationRecord.target,类型是 Node,根据 MutationRecord.type,返回变化所影响的节点。对于属性 attributes 变化,返回属性变化的节点。对于 characterData 变化,返回 characterData 节点。对于子节点树 childList 变化,返回子节点变化的节点
      • MutationRecord.addedNodes,类型是 NodeList,返回被添加的节点。如果没有节点被添加,则该属性将是一个空的 NodeList
      • MutationRecord.removedNodes,类型是 NodeList,返回被移除的节点。如果没有节点被移除,则该属性将是一个空的 NodeList
      • MutationRecord.previousSibling 类型是 Node,返回被添加或移除的节点之前的兄弟节点,或者 null
      • MutationRecord.nextSibling 类型是 Node 返回被添加或移除的节点之后的兄弟节点,或者 null
      • MutationRecord.attributeName 类型是 string 返回被修改的属性的属性名,或者 null
      • MutationRecord.attributeNamespace 类型是 string 返回被修改属性的命名空间,或者 null
      • MutationRecord.oldValue 类型是 string 返回值取决于 MutationRecord.type。对于属性 attributes 变化,返回变化之前的属性值。对于 characterData 变化,返回变化之前的数据。对于子节点树 childList 变化,返回 null注意如果要让这个属性起作用,在相应的 MutationObserverInit 参数的MutationObserver observe 方法中, attributeOldValue 或者 characterDataOldValue 必须设置 `true`
  5. observe(target, config)
    • observe 方法传入需要监听的节点,以及监听属性 config
    • config
      • attributes 布尔类型,属性的变动
      • childList 布尔类型,子节点的变动(指新增,删除),不包括修改子节点以及子节点后代的变化
      • characterData 布尔类型 节点内容或节点文本的变动
      • subtree 布尔类型 是否将该观察器应用于该节点的所有后代节点
      • attributeOldValue 布尔类型 观察 attributes 变动时,是否需要记录之前变动的值,如果设置了他可以省略 attributes 的设置,默认是 true
      • characterDataOldValue 布尔类型,观察 characterData 变动时是否需要记录变动前的值,同理,如果设置了他可以省略 characterData 的设置
      • attributeFilter 数组 需要观察的特定属性,比如 ['class', 'src']
  6. MutationObserver 特点
    • 等待所有脚本任务完成之后才会运行,采用异步方式
    • dom 变动封装成一个数组进行处理,而不是一条条地个别处理 dom 变动
    • 它既可以观察发生在 dom 节点的所有变动,也可以观察某一类变动
    • dom 发生变动会触发 MutationObserver 事件,但是,它与事件有一个本质不同,事件是同步触发,也就是说 dom 发生变动立即就会触发相应的事件,MutationObserver 则是异步触发,dom 发生变化以后,并不会马上触发,而是要等到所有 dom 操作都结束后才触发
    • 举例来说,如果在文档中连续插入 1000 个段落(p 元素),会连续触发 1000 个插入事件,执行每个事件的回调函数,这很可能造成浏览器的卡顿;而 MutationObserver 完全不同,只在 1000 个段落都插入结束后才会触发,而且只触发一次,这样较少了 DOM 的频繁变动,大大有利于性能
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let targetNode = document.getElementById('root');

let config = {
attributes: true,
childList: true,
subtree: true
};

let callback = function(mutationRecords, observer) {
for (var mutation of mutationRecords) {
if (mutation.type === 'childList') {
console.log("A child node has been added or removed")
} else if (mutation.type === 'attributes') {
console.log('The ' + mutation.attributeName + ' attribute was modified');
}
}
};

let observer = new MutationObserver(callback);

observer.observe(targetNode, config);

observer.disconnect();

ResizeObserver

  1. 用来监听元素尺寸变化,之前为了监听元素的尺寸变化,都将监听附加到 window 中的 resize 事件,对于不受窗口变化影响的元素就没这么简单了
  2. 构造函数 ResizeObserver(callback),创建并返回一个新的 ResizeObserver 对象
  3. callback 在内容盒或边框盒边界尺寸发生变化时调用
    • 该回调函数接收两个参数
      • entries 一个 ResizeObserverEntry 对象数组,可以用于获取每个元素改变后的新尺寸,ResizeObserverentry 属性如下
        • ResizeObserverEntry.borderBoxSize 只读属性,一个对象,当运行回调时,该对象包含着正在观察元素的新边框盒的大小
        • ResizeObserverEntry.contentBoxSize 只读属性,一个对象,当运行回调时,该对象包含着正在观察元素的新内容盒的大小
        • ResizeObserverEntry.devicePixelContentBoxSize 只读属性,一个对象,当运行回调时,该对象包含着正在观察元素的新内容盒的大小(以设备像素为单位)
        • ResizeObserverEntry.contentRect 只读属性,一个对象,当运行回调时,该对象包含着正在观察元素新大小的 DOMRectReadOnly 对象。请注意,这比以上两个属性有着更好的支持,但是它是 Resize Observer API 早期实现遗留下来的,出于对浏览器的兼容性原因,仍然被保留在规范中,并且在未来的版本中可能被弃用
        • ResizeObserverEntry.target 只读属性,对正在观察 ElementSVGElement 的引用
      • observerResizeObserver 自身的引用,因此需要它的时候,你要从回调函数的内部访问。例如,这可用于在达到特定的情况时,自动取消对观察者的监听,但如果你不需要它,可以省略它

内容盒是盒模型放置内容的部分,这意味着边框盒减去内边距和边框的宽度就是内容盒。边框盒包含内容、内边距和边框

  1. 方法
    • disconnect() 取消特定观察者目标上所有对 element 的监听
    • observe(target[, options]) 开始对指定 element 的监听
      • target 是要监听的元素
      • options 是一个可选参数,允许为监听的对象设置参数,目前只有一个参数
        • box 设置 observer 将监听的盒模型,可能的值
          • content-bxo 默认,css 中定义的内容区域的大小
          • border-box css 中定义的边框区域的大小
          • device-pixel-content-box 在对元素或其祖先应用任何 CSS 转换之前,CSS 中定义的内容区域的大小,以设备像素为单位
    • unobserve(target) 结束对指定 element 的监听
  2. 触发条件
    • 在内容盒或边框盒边界尺寸发生变化时触发
    • 元素被插入或移除时触发
    • 元素 display 从显示变成 none 或相反过程时触发
  3. 何时不触发
    • 对于不可替换内联元素不触发
    • css transform 操作不触发
  4. api 依然处于试验阶段,需要考虑其兼容性

PerformanceObserver

  1. PerformanceObserver 是一个相对比较复杂的 api 用来监控各种性能相关的指标。 该 API 由一系列 API 组成:
  2. 构造函数
    • PerformanceObserver(callback)
    • callback 为回调函数接收如下两个参数
      • 第一个参数是 性能观察条目列表 PerformanceObserverEntryList
        • PerformanceObserverEntryList.getEntries() 返回一个 PerformanceEntry 对象数组
        • PerformanceObserverEntryList.getEntriesByType(type) 根据 type 返回一个 PerformanceEntry 所构成的对象数组
        • PerformanceObserverEntryList.getEntriesByName(name[, type]) 根据 nametyp返回一个 PerformanceEntry 所构成的对象数组
      • 第二个参数是 观察者对象 observer
    • PerformanceEntry 对象代表了 performance 时间列表中的单个 metric 数据。每一个 performance entry 都可以在应用运行过程中通过手动构建 mark 或者 measure (例如调用 mark() 方法) 生成。此外,Performance entries 在资源加载的时候,也会被动生成(例如图片、script、css 等资源加载),该对象包含如下属性和方法
      • PerformanceEntry.name 只读,该 PerformanceEntry 的名字
      • PerformanceEntry.entryType 只读, DOMString 代表所上报的 performance metricentryType 类型,例如 “mark”,其他类型可查阅 EntryType
      • PerformanceEntry.startTime 只读,为 metric 上报的时间
      • PerformanceEntry.duration 该事件的耗时
      • PerformanceEntry.toJSON() 返回 PerformanceEntry 对象的 JSON 格式数据
    • 开始监听 observer.observe({entryTypes: [entryTypes]})
      • entryTypes: 需要监控的指标名,这些指标都可以通过 performance.getEntries() 获取到,此外还可以通过 performance.getEntriesByName()performance.getEntriesByType() 分别针对 nameentryType 来过滤
        • mark 获取所有通过 performance.mark(markName) 做的所有标记
        • measure 获取通过 performance.measure(measureName, markName_start, markName_end) 得到的所有测量值
        • longtask 监听长任务(超过 50ms 的任务)(不足:只能监控到长任务的存在,貌似不能定位到具体任务)
        • paint 获取绘制相关的性能指标,分为两种:first-paintfirst-contentful-paint
        • navigation 各种与页面有关的时间,可通过 performance.timing 获取
        • resource 各种与资源加载相关的信息
1
2
3
4
5
6
7
8
9
10
11
const observer = new PerformanceObserver((list) => {
let output;
for (const item of list.getEntries()) {
//业务代码
}
});

observer.observe({
//按需要填写
entryTypes: ['mark', 'measure', 'longtask', 'paint', 'navigation', 'resource']
});