IntersectionObserver实现高性能的交互动画

背景:以前我们在写滚动动画的时候通常判断元素是否显示通常会通过 getBoundingClientRect 获取位置,但是 getBoundingClientRect 将触发重排,利用此技术可能会很快造成性能瓶颈。

关于 IntersectionObserver

IntersectionObserver 是 web 领域众多观察器中的一个,是用来是观察元素和窗体相交的状态,很适合用在滚动交互事件,像是懒加载、埋点等场景。

除了 IntersectionObserver ,我们常用的还有用来观察 DOM 变化的 MutationObserver;用来观察元素的尺寸变化 ResizeObserver。

使用

  • 创建一个 intersection observer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const callback = (entries) => {
const [entry] = entries;
if (entry.isIntersecting) {
console.log("元素曝光了");
}
};

const options = {
root: document.querySelector("#scrollArea"), // 指定根(root)元素,必须是目标元素的父级元素,如果未指定或者为null,则默认为浏览器视窗。
rootMargin: "0px", // 根(root)元素的外边距
threshold: 1.0, // 阈值为1.0意味着目标元素完全出现在root选项指定的元素中可见时,回调函数将会被执行。
};
const observer = new IntersectionObserver(callback, options);

const ele = document.querySelector(".animatedElement");

observer.observe(ele);

callback 回调函数将会在主线程中被执行, 其中 entry 属性:

1
2
3
4
5
6
7
entry.boundingClientRect 目标元素的区域信息
entry.intersectionRatio 目标元素的可见比率
entry.intersectionRect 目标元素与根元素交叉的区域信息
entry.isIntersecting 是否进入可视区域
entry.rootBounds 根元素的矩形区域信息
entry.target 被观察的目标,是一个DOM节点
entry.time 可见性发生变化的时间,相交发生时距离页面打开时的毫秒数.精度为微秒。

Intersection Observer 可用的方法有 observe(),unobserve() 和 disconnect()。

  • observe():用来添加观察者要监视的目标元素,观察者可以具有多个目标元素,但是此方法一次只能接受一个目标。
1
2
const element = document.querySelector('.animatedElement');
observer.observe(element);
  • unobserve():用来从观察的元素列表中移除元素。
1
observer.unobserve(element);
  • disconnect():用来停止观察其所有目标元素。观察者本身仍处于活动状态,但没有目标。在 disconnect() 之后,目标元素仍然可以通过 observe() 传递给观察者。
1
observer.disconnect();

利用 getBoundingClientRect 实现动画

通常是监听滚动,通过获取元素位置

1
2
window.addEventListener("scroll", () => checkForVisibility);
window.addEventListener("resize", () => checkForVisibility);

根据元素的位置触发动画

1
2
3
4
5
6
7
8
function checkForVisibility() {
const element = document.querySelector(".animatedElement");
const distTop = element.getBoundingClientRect().top;
const distBottom = element.getBoundingClientRect().bottom;
const distPercentTop = Math.round((distTop / window.innerHeight) * 100);
const distPercentBottom = Math.round((distBottom / window.innerHeight) * 100);
// do something
}

但是 getBoundingClientRect 将触发重排也就会造成性能问题,那 getBoundingClientRect() 为什么会触发 Reflow 呢?
在 chromium 的源码中搜索 getBoundingClientRect 可以看到代码:

1
2
3
DOMRect* Range::getBoundingClientRect() const {
return DOMRect::FromFloatRect(BoundingRect());
}

BoundingRect() 中调用了 UpdateStyleAndLayout

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
FloatRect Range::BoundingRect() const {
owner_document_->UpdateStyleAndLayout(DocumentUpdateReason::kJavaScript);

Vector<FloatQuad> quads;
GetBorderAndTextQuads(quads);

FloatRect result;
for (const FloatQuad& quad : quads)
result.Unite(quad.BoundingBox()); // Skips empty rects.

// If all rects are empty, return the first rect.
if (result.IsEmpty() && !quads.IsEmpty())
return quads.front().BoundingBox();

return result;
}

其中 UpdateStyleAndLayout 方法调用之后将会触发 LayoutTree 的重新渲染,也就是 Reflow。

利用 IntersectionObserver 实现动画

通过 Intersection Observer API 获取到两个元素重叠部分的准确值,只需几行代码即可设置根据元素可见性触发动画:

1
2
3
4
5
6
7
8
9
10
const animationObserver = new IntersectionObserver((entries, observer) => {
for (const entry of entries) {
// do something
entry.target.classList.toggle("animating", entry.isIntersecting);
}
});

for (const element of querySelectorAll(".animatedElement")) {
animationObserver.observe(element);
}

IntersectionObserver-polyfill

对于不支持的浏览器可以引入 w3c 官方创建的 polyfill https://github.com/w3c/IntersectionObserver。

参考资料

https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API
https://www.zhangxinxu.com/wordpress/2020/12/js-intersectionobserver-nav/