JS63 IntersectionObserver API

Intersection Observer API 学习笔记。

元素可见性

页面的可见性可以用document.visibilityState或者document.hidden获得,通过document.visibilitychange来监听页面可见性的变化,但是对于页面的元素的可见性却只能手动通过位置判断。

例如下面这个例子

需要监听scroll事件,根据target元素的是否出现,来更改顶部的文字和样式。由于target内嵌了几层,所以判断时需要通过getBoundingClientRect方法获得其位置,与视口位置比较,比较的标的还需要根据其父元素的位置变化,实现起来还是有一点麻烦的:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
<div class="container">
<header class="head" id="head"></header>
<main class="main" id="main">
<div class="content">
<div class="target-container" id="targetContainer">
<div class="target" id="target">
</div>
</div>
</div>
</main>
<footer class="foot" id="foot"></footer>
</div>

<script>
window.onload = () => {
const head = document.querySelector('#head'),
foot = document.querySelector('#foot'),
main = document.querySelector('#main'),
targetContainer = document.querySelector('#targetContainer'),
target = document.querySelector('#target');

const footTop = foot.getBoundingClientRect().top;

// 改变 head 的文字和样式
const changeHeadState = isVisible => {
head.textContent = isVisible ? 'Visible' : 'Invisible';
head.setAttribute('style', `background: ${isVisible ? 'lightgreen' : 'red'}`)
};

const watchPosition = () => {
const targetContainerRect = targetContainer.getBoundingClientRect(),
targetContainerBottom = targetContainerRect.top + targetContainerRect.height;

const targetTop = target.getBoundingClientRect().top;

let isVisible = false;

// targetContainer 完全可见
if (targetContainerBottom < footTop) {
isVisible = targetTop < targetContainerBottom;
} else {
isVisible = targetTop < footTop;
}

return isVisible
};

changeHeadState(watchPosition());

main.addEventListener('scroll', () => {
changeHeadState(watchPosition());
});

targetContainer.addEventListener('scroll', () => {
changeHeadState(watchPosition());
});
}
</script>

IntersectionObserver API

现在有了一个新的API来帮助我们简化这个工作,那就是IntersectionObserver API,它的兼容性如下:

使用的时候首先需要新建一个实例:

1
const io = new IntersectionObserver(callback, option);

其中callback是监听元素的可见性发生变化时的回调函数,它会在元素进入视口(开始可见)或者完全离开视口(开始不可见)时被触发,它的参数是一个数组,每一项都是一个监听对象对应的IntersectionObserverEntry对象,它主要包含以下属性:

  • time,可见性发生变化的时间,是一个高精度时间戳,单位为毫秒
  • target,被观察的目标元素,DOM节点
  • rootBounds,根元素的getBoundingClientRect()的返回值
  • boundingClientRect,目标元素的getBoundingClientRect()的返回值
  • intersectionRect,目标元素与视口(或根元素)交叉区域getBoundingClientRect()的返回值
  • intersectionRatio,目标元素的可见比例,即intersectionRectboundingClientRect的比例,完全可见时为1,完全不可见时小于等于0

在Chrome 78版本中会返回isVisible属性,但是不知道是不是Bug,无论元素是否可见,都为false,但是isTntersecting的表现是正常的,所以判断是否可见,可以根据intersectionRatio或者isTntersecting来进行判断

构造函数的第二个参数option是一个配置对象,可以配置的属性包括rootrootMarginthreshold,具体配置方法参考文档

通过构造函数新建了一个观察器实例,实例的observe方法可以观察传入的DOM节点:

1
2
3
4
5
6
7
8
// 开始观察
io.observe(document.getElementById('example'));

// 停止观察
io.unobserve(element);

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

如果要观察多个对象,就需要多次调用observe方法。

注意,IntersectionObserver API是异步的,不随着目标元素的滚动同步触发。IntersectionObserver的实现,应该采用requestIdleCallback(),即只有线程空闲下来,才会执行观察器。这意味着,这个观察器的优先级非常低,只在其他任务执行完,浏览器有了空闲才会执行。

Demo

可以使用IntersectionObserver API重写上面的例子,简化判断元素可见的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
window.onload = () => {
const head = document.querySelector('#head'),
target = document.querySelector('#target');

// 改变 head 的文字和样式
const changeHeadState = isVisible => {
head.textContent = isVisible ? 'Visible' : 'Invisible';
head.setAttribute('style', `background: ${isVisible ? 'lightgreen' : 'red'}`)
};

const io = new IntersectionObserver(([obj]) => {
console.log(obj);
const isVisible = obj.intersectionRatio > 0;
changeHeadState(isVisible)
});

io.observe(target);
}

简单的无线滚动

可以使用这个API,实现一个简单的无线滚动的Demo

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
<div class="container">
<header class="head" id="head"></header>
<main class="main" id="main">
<ul class="content" id="content">
</ul>
<div class="target" id="target"></div>
</main>
<footer class="foot" id="foot"></footer>
</div>
<script>
window.onload = () => {
const content = document.querySelector('#content'),
head = document.querySelector('#head'),
target = document.querySelector('#target');

// 模拟网络请求
const fetch = () => {
return new Promise(resolve => {
setTimeout(resolve, 2000, 10)
})
};

// 改变 head 的文字和样式
const getNewContent = (() => {
// 标志服,避免重复请求
let pending = false;
const frag = document.createDocumentFragment();

return isVisible => {
if (!isVisible) {
return;
}

if (pending) {
return;
}

pending = true;
head.textContent = 'Loading...';

fetch().then(num => {
for (let i = 0; i < num; i++) {
const li = document.createElement('li');
li.classList.add('item');
frag.appendChild(li)
}
content.appendChild(frag);

pending = false;
head.textContent = 'Done!';
})
};
})();

const io = new IntersectionObserver(([obj]) => {
const isVisible = obj.intersectionRatio > 0;
getNewContent(isVisible)
});

io.observe(target);
}
</script>

在列表的末尾放置一个标志元素,当它可见的时候,就去触发网络请求,获取新的数据。

参考