# Intersection Observer

当你想监听某个元素,当它在视口中可见的时候希望可以得到通知,这个API就是最佳的选择了。以往我们的做法是绑定容器的scroll事件,或者设定时器不停地调用getBoundingClientRect() 获取元素位置, 这样做的性能会很差,因为每次获取元素的位置都会引起整个布局的重新计算。还有一个场景是,如果你的元素被放在iframe里,如一些广告,想要知道他们何时出现几乎是不可能的。

  • IntersectionObserver接口 提供了一种异步观察目标元素与其祖先元素或顶级文档视窗(viewport)交叉状态的方法。祖先元素与视窗(viewport)被称为根(root)

  • 通俗点说就是:IntersectionObserver是用来监听某个元素与视口的交叉状态的。交叉状态是什么呢?请看下图,一开始整个元素都在视口内,那么元素与视口的交叉状态就是100%,而我往下滚动,元素只有一半显示在视口里,那么元素与视口的交叉状态为50%:

# 用法

// 接收两个参数 callback  option
var io = new IntersectionObserver(callback, option);

// 开始观察(可观察多个元素)
io.observe(document.getElementById('example1'));
io.observe(document.getElementById('example2'));

// 停止观察某个元素
io.unobserve(element);

// 关闭观察器
io.disconnect();

1
2
3
4
5
6
7
8
9
10
11
12
13
  • callback

callback一般有两种触发情况。一种是目标元素刚刚进入视口(可见),另一种是完全离开视口(不可见)

var io = new IntersectionObserver(
  entries => {
    console.log(entries);
  }
);
1
2
3
4
5

callback函数的参数(entries)是一个数组,每个成员都是一个IntersectionObserverEntry对象。举例来说,如果同时有两个被观察的对象的可见性发生变化,entries数组就会有两个成员。

  • IntersectionObserverEntry对象

    • time:可见性发生变化的时间,是一个高精度时间戳,单位为毫秒
    • target:被观察的目标元素,是一个 DOM 节点对象
    • rootBounds:根元素的矩形区域的信息,getBoundingClientRect()方法的返回值,如果没有根元素(即直接相对于视口滚动),则返回null
    • boundingClientRect:目标元素的矩形区域的信息
    • intersectionRect:目标元素与视口(或根元素)的交叉区域的信息
    • intersectionRatio:目标元素的可见比例,即intersectionRect占boundingClientRect的比例,完全可见时为1,完全不可见时小于等于0
{
  time: 3893.92,
  rootBounds: ClientRect {
    bottom: 920,
    height: 1024,
    left: 0,
    right: 1024,
    top: 0,
    width: 920
  },
  boundingClientRect: ClientRect {
     // ...
  },
  intersectionRect: ClientRect {
    // ...
  },
  intersectionRatio: 0.54,
  target: element
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  • option

讲讲第二个参数option里比较重要的两个属性:threshold和root

首先讲讲threshold:

threshold属性决定了什么时候触发回调函数。它是一个数组,每个成员都是一个门槛值,默认为[0],即交叉比例(intersectionRatio)达到0时触发回调函数。

new IntersectionObserver(
  entries => {/* ... */}, 
  {
    threshold: [0, 0.25, 0.5, 0.75, 1],
    root: document.getElementById('#container')
  }
);

//用户可以自定义这个数组。比如,[0, 0.25, 0.5, 0.75, 1]就表示当目标元素 0%、25%、50%、75%、100% 可见时,会触发回调函数。
//再说说root:
//IntersectionObserver API 支持容器内滚动。root属性指定目标元素所在的容器节点(即根元素)。注意,容器元素必须是目标元素的祖先节点。

1
2
3
4
5
6
7
8
9
10
11
12
  • 完整例子
body {
            height: 3000px;
            width: 3000px;
        }

#box1 {
            width: 300px;
            height: 300px;
            background-color: red;
            margin-top: 100px;
            margin-left: 300px;
        }
#box2 {
            width: 300px;
            height: 300px;
            background-color: red;
            margin-top: 100px;
            margin-left: 300px;
        }
<div id="box1"></div>
<div id="box2"></div>

const io = new IntersectionObserver(entries => {
            console.log(entries)
        }, {
            threshold: [0, 0.25, 0.5, 0.75, 1]
            // root: xxxxxxxxx
        })
io.observe(document.getElementById('box1'))
io.observe(document.getElementById('box2'))

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
  • 使用场景
  1. 可以像getBoundingClientRect那样判断元素是否在视口里,并且好处是,不会引起重绘回流
  2. 同理,有了第一点功能,就可以做懒加载和无限滚动功能了

# Mutation Observer

当我们想知道某个元素在某个时候发生了具体哪些变化时,MutationObserver便是最佳选择了。

var observer = new MutationObserver(callback);
observer.observe(target, config);

1
2
3
  • config 填写需要监听属性

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

methods: {
    observerCallBack (mutations) {
    	//do log
    },

    onAddAttr () {
    	// toggle attribute 'd'
    }
},

mounted () {
    this.$list = document.querySelector('.js-list');

    let config = {
    	attributes: true, 
    	childList: true, 
    	characterData: true,
    	subtree: true
    };
    let observer = new MutationObserver(this.observerCallBack);
    observer.observe(this.$list, config);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# Resize Observer

从名字就可以知道该API是干嘛的了:监听元素的尺寸变化。

之前为了监听元素的尺寸变化,都将侦听器附加到window中的resize事件。对于不受窗口变化影响的元素就没那么简单了。 现在我们可以使用该API轻松的实现。

var observer = new ResizeObserver(callback);
observer.observe(target);
1
2
  • 触发

    1. 元素被插入或移除时触发
    2. 元素display从显示变成 none 或相反过程时触发
  • 不触发

    1. 对于不可替换内联元素不触发
    2. CSS transform 操作不触发

遗憾的是该API仍处于实验阶段,好多浏览器没有实现。 由于MutationObserver已经被大部分浏览器支持,且有polyfill的支持, 我们可以轻松的利用他来代替ResizeObserver。

# Performance Observer

PerformanceObserver 是个相对比较复杂的API,用来监控各种性能相关的指标。 该API由一系列API组成:

  • Performance Timeline Level 2

  • Paint Timing 1

  • Navigation Timing Level 2

  • User Timing Level 3

  • Resource Timing Level 2

  • Long Tasks API 1

  • 如何使用

var observer = new PerformanceObserver(callback);
observer.observe({ entryTypes: [entryTypes] });

//监控相关指标
const observer = new PerformanceObserver((list) => {
   let output;
   for (const item of list.getEntries()) {
       //业务代码
   }
});

observer.observe({
    //按需要填写
    entryTypes: ['mark', 'measure', 'longtask', 'paint', 'navigation', 'resource'] 
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  • entryTypes: 需要监控的指标名,这些指标都可以通过 performance.getEntries() 获取到,此外还可以通过 performance.getEntriesByName() 、performance.getEntriesByType() 分别针对 name 和 entryType 来过滤。

    • mark 获取所有通过 performance.mark(markName) 做的所有标记
    • measure 获取通过 performance.measure(measureName, markName_start, markName_end) 得到的所有测量值
    • longtask 监听长任务(超过50ms 的任务)(不足:只能监控到长任务的存在,貌似不能定位到具体任务)
    • paint 获取绘制相关的性能指标,分为两种:“first-paint”、“first-contentful-paint”
    • navigation 各种与页面有关的时间,可通过 performance.timing 获取
    • resource 各种与资源加载相关的信息

# 参考

JS中的观察者们 —— 四种 Observers (opens new window)

浏览器的 5 种 Observer,你用过几种? (opens new window)