Skip to content
文章目录

现代浏览器观察者 Observer API

总体对比

IntersectionObserverMutationObserverResizeObserverPerformanceObserver
用途观察一个元素是否在视窗可见观察 DOM 中的变化观察视口大小的变化监测性能度量事件
方法observe()
disconnect()
takeRecords()
observe()
disconnect()
takeRecords()
unobserve()
observe()
disconnect()
unobserve()
observe()
disconnect()
takeRecords()
取代Dom Mutation eventsgetBoundingClientRect() 返回元素的大小及其相对于可视窗口的位置,Scroll 和 Resize 事件Resize 事件Performance 接口
用途1. 无限滚动
2. 图片懒加载
3. 兴趣埋点
4. 控制动画/视频执行(性能优化)
1. 更高性能的数据绑定及响应
2. 实现视觉差滚动
3. 图片预加载
4. 实现富文本编辑器
1. 更智能的响应式布局(取代@media)
2. 响应式组件
1. 更细颗粒的性能监控
2. 分析性能对业务的影响(交互快/慢是否会影响销量)

Intersection Observer,交叉观察者

IntersectionObserver 可以监听一个元素和可视区域相交部分的比例,然后在可视比例达到某个阈值的时候触发回调。

浏览器兼容性

语法

ts
var observer = new IntersectionObserver(callback[, options]);
  • callback 是一个回调函数,里面返回监听目标元素的实时数据组成的数组
    • time 时间戳
    • rootBounds 根元素的位置信息
    • boundingClientRect 目标元素的位置信息
    • intersectionRect 交叉部分的位置信息
    • intersectionRatio 目标元素的可见比例,看下图示
    • target 等
  • options 是一些配置
    • root 目标元素的祖先元素,即该元素必须是目标元素的直接或间接父级
    • rootMargin 一个在计算交叉值时添加至 root 的边界盒中的一组偏移量,写法类似 CSS 的 margin
    • threshold 规定了一个监听目标与边界盒交叉区域的比例值,可以是一个具体的数值或是一组 0.0 到 1.0 之间的数组

开始监听元素: observer.observe(target)

停止对某目标的监听: observer.unobserve(target)

终止对所有目标的监听: observer.disconnect()

polyfill

pnpm add intersection-observer

使用

入口文件引入即可

import 'intersection-observer'

示例

vue
<script setup>
const intersectionObserver = new IntersectionObserver(
  function (entries) {
    console.log('info:')
    entries.forEach(item => {
      console.log(item.target, item.intersectionRatio)
    })
  },
  {
    threshold: [0.5, 1],
  }
)
</script>

<template>
  <div id="box1">BOX111</div>
  <div id="box2">BOX222</div>
</template>

<style>
#box1,
#box2 {
  width: 100px;
  height: 100px;
  background: blue;
  color: #fff;

  position: relative;
}
#box1 {
  top: 500px;
}
#box2 {
  top: 800px;
}
</style>

创建一个 IntersectionObserver 对象,监听 box1 和 box2 两个元素,当可见比例达到 0.5 和 1 的时候触发回调。

可以看到元素 box1 和 box2 在可视范围达到一半(0.5)和全部(1)的时候分别触发了回调。

数据采集的时候,希望知道某个元素是否是可见的,什么时候可见的,就可以用这个 api 来监听,还有做图片的懒加载的时候,可以当可视比例达到某个比例再触发加载。

Mutation Observer,变动观察者

MutationObserver 可以监听对元素的属性的修改、对它的子节点的增删改。

兼容性

语法

ts
var observer = new MutationObserver(callback)

开始监听

ts
observer.observe(target, config)
  • config 填写需要监听属性
    • attributes 布尔类型 属性的变动
    • childList 布尔类型 子节点的变动(指新增,删除或者更改)
    • characterData 布尔类型 节点内容或节点文本的变动。
    • subtree 布尔类型 是否将该观察器应用于该节点的所有后代节点
    • attributeOldValue 布尔类型 观察 attributes 变动时,是否需要记录变动前的属性值
    • characterDataOldValue 布尔类型 观察 characterData 变动时,是否需要记录变动前的值
    • attributeFilter 数组 需要观察的特定属性(比如['class','src'])

终止对所有目标的监听: observer.disconnect()

takeRecords(): 从 MutationObserver 的通知队列中删除所有待处理的通知,并将它们返回到 MutationRecord 对象的新 Array 中。

polyfill

如果你的浏览器不需要兼容 IE9,IE10 浏览器,推荐使用 MutationObserver 实现 DOM 变化的检测。

如果你的项目需要兼容 IE9,IE10 浏览器,同时想要实现对 DOM 变化的检测,则可以试试 Mutation events。

Mutation events 语法上相对简单易懂很多。

你就认为是和'click', 'mouseover'一样的 DOM 事件用就好了。

支持的事件列表如下:

  • DOMAttrModified (Chrome/Safari 不支持)
  • DOMAttributeNameChanged
  • DOMCharacterDataModified
  • DOMElementNameChanged
  • DOMNodeInserted
  • DOMNodeInsertedIntoDocument (IE 不支持)
  • DOMNodeRemoved
  • DOMNodeRemovedFromDocument (IE 不支持)
  • DOMSubtreeModified

具体描述见下表(IE 不支持的两个我们忽略,这个就算没兼容性问题和很少用到):

事件名称事件描述
DOMAttrModifiedDOM 属性发生修改
DOMAttributeNameChangedDOM 属性名发生变化
DOMCharacterDataModifiedDOM 文本数据发生修改
DOMElementNameChangedDOM 元素名发生变化
DOMNodeInsertedDOM 节点插入
DOMNodeRemovedDOM 节点删除
DOMSubtreeModifiedDOM 子元素修改

使用例子:

js
element.addEventListener(
  'DOMNodeInserted',
  function (event) {
    // event.target就是依次插入的DOM节点
  },
  false
)

Chrome/Safari 不支持 DOMAttrModified 的处理

Object.defineProperty,可以方便对自定义属性的变化进行检测。

ts
// 重新定义rows属性
Object.defineProperty(ell, 'rows', {
  writeable: true,
  enumerable: true,
  get: function () {
    return this.getAttribute('rows')
  },
  set: function (rows) {
    this.setAttribute('rows', rows)
    // rows变化了,重渲染
    this.render()
  },
})

Object.defineProperty 语法如下:

ts
Object.defineProperty(obj, prop, descriptor)

示例

vue
<script setup>
setTimeout(() => {
  box.style.background = 'red'
}, 2000)

setTimeout(() => {
  const dom = document.createElement('button')
  dom.textContent = '东东东'
  box.appendChild(dom)
}, 3000)

setTimeout(() => {
  document.querySelectorAll('button')[0].remove()
}, 5000)

onMounted(() => {
  const mutationObserver = new MutationObserver(mutationsList => {
    console.log(mutationsList)
  })

  mutationObserver.observe(box, {
    attributes: true,
    childList: true,
  })
})
</script>

<template>
  <div id="box"><button></button></div>
</template>

<style>
#box {
  width: 100px;
  height: 100px;
  background: blue;

  position: relative;
}
</style>

2s 的时候修改背景颜色为红色,3s 的时候添加一个 button 的子元素,5s 的时候删除第一个 button。

第一次改变的是 attributes,属性是 style:

第二次改变的是 childList,添加了一个节点:

第三次也是改变的 childList,删除了一个节点:

比如文章水印被人通过 devtools 去掉了,那么就可以通过 MutationObserver 监听这个变化,然后重新加上,让水印去不掉。

Resize Observer,尺寸观察者

元素可以用 ResizeObserver 监听大小的改变,当 width、height 被修改时会触发回调。

浏览器兼容性

语法

ts
var observer = new ResizeObserver(callback)
observer.observe(target)

开始监听元素: observer.observe(target)

停止对某目标的监听: observer.unobserve(target)

终止对所有目标的监听: observer.disconnect()

触发回调后的第一个参数是一个 ResizeObserverEntry 对象。这里的 entry.target 是 DOM 节点本身,而 entry.contentRect 是一个对象,包含了节点的位置属性,如 width, height, left, right, bottom, left, x, y 等。

  • width:指元素本身的宽度,不包含 padding,border 值
  • height:指元素本身的高度,不包含 padding,border 值
  • top:指 padidng-top 的值
  • left:指 padding-left 的值
  • right:指 left + width 的值
  • bottom: 值 top + height 的值
  • x:大小与 top 相同
  • y:大小与 left 相同

polyfill

pnpm add resize-observer-polyfill
ts
import ResizeObserver from 'resize-observer-polyfill'

const ro = new ResizeObserver((entries, observer) => {
  for (const entry of entries) {
    const { left, top, width, height } = entry.contentRect

    console.log('Element:', entry.target)
    console.log(`Element's size: ${width}px x ${height}px`)
    console.log(`Element's paddings: ${top}px ; ${left}px`)
  }
})

ro.observe(document.body)

示例

vue
<script setup>
onMounted(() => {
  const box = document.querySelector('#box')

  const resizeObserver = new ResizeObserver(entries => {
    console.log('当前大小', entries)
  })
  resizeObserver.observe(box)

  setTimeout(() => {
    box.style.width = '200px'
  }, 3000)
})
</script>

<template>
  <div id="box"></div>
</template>

<style>
#box {
  width: 100px;
  height: 100px;
  background: blue;
}
</style>

大小变化被监听到了,看下打印的信息

可以拿到元素和它的位置、尺寸。

Performance Observer,性能观察者

PerformanceObserver 用于监听记录 performance 数据的行为,一旦记录了就会触发回调,这样我们就可以在回调里把这些数据上报。

比如 performance 可以用 mark 方法记录某个时间点:

ts
performance.mark('registered-observer')

用 measure 方法记录某个时间段:

ts
performance.measure('button clicked', 'from', 'to')

后两个个参数是时间点,不传代表从开始到现在。

我们可以用 PerformanceObserver 监听它们:

html
<html>
  <body>
    <button onclick="measureClick()">Measure</button>

    <img src="https://p9-passport.byteacctimg.com/img/user-avatar/4e9e751e2b32fb8afbbf559a296ccbf2~300x300.image" />

    <script>
      const performanceObserver = new PerformanceObserver(list => {
        list.getEntries().forEach(entry => {
          console.log(entry) // 上报
        })
      })
      performanceObserver.observe({ entryTypes: ['resource', 'mark', 'measure'] })

      performance.mark('registered-observer')

      function measureClick() {
        performance.measure('button clicked')
      }
    </script>
  </body>
</html>

创建 PerformanceObserver 对象,监听 mark(时间点)、measure(时间段)、resource(资源加载耗时) 这三种记录时间的行为。

然后我们用 mark 记录了某个时间点,点击 button 的时候用 measure 记录了某个时间段的数据,还加载了一个图片。

当这些记录行为发生的时候,希望能触发回调,在里面可以上报。

mark

resource

measure

参考资料

浏览器的 5 种 Observer,你用过几种?

现代浏览器观察者 Observer API 指南

JS 中的观察者们 —— 四种 Observers

IntersectionObserver API 应用总结

懒加载核心实现之 intersectionobserver 交叉观察器踩坑

聊聊 JS DOM 变化的监听检测与应用