比如说,你想跟踪 DOM 树里的一个元素,当它进入可见窗口时得到通知。 也许想实现即时延迟加载图片功能,或者你需要知道用户是否真的在看一个广告 banner。 你可以通过绑定 scroll
事件或者用一个周期性的定时器,然后再回调函数中调用元素的 getBoundingClientRect()
获取元素位置实现这个功能。 但是,这种实现方式性能极差,因为每次调用 getBoundingClientRect()
都会强制浏览器重新计算整个页面的布局,可能给你的网站造成相当大的闪烁。 如果你的站点被加载到一个 iframe 里,而你想要知道用户什么时候能看到某个元素,这几乎是不可能的。 单原模型(Single Origin Model)和浏览器不会让你获取 iframe 里的任何数据。 这对于经常在 iframe 里加载的广告页面来说是一个很常见的问题。
IntersectionObserver 就是为此而生的,它让检测一个元素是否可见更加高效,而且已经在 Chrome 51 中实现。 IntersectionObserver
能让你知道一个被观测的元素什么时候进入或离开浏览器的可见窗口。
如何创建一个 IntersectionObserver
API 比较简单,最好用一个例子说明:
var io = new IntersectionObserver( entries => { console.log(entries); }, { /* 使用默认参数。下面详细说明 */ } ); // 开始观测某个元素 io.observe(element); // 停止关注某个元素 // io.unobserve(element); // 禁用整个 IntersectionObserver // io.disconnect();
使用 IntersectionObserver
的默认属性,当元素部分进入可见窗口或完全离开可见窗口时都会调用你的回调函数。
如果你需要观测多个元素,你可以用——而且是推荐使用——同一个IntersectionObserver
实例调用多次 observe()
。
一个 entries 参数会被传递给你的回调函数,它是一个IntersectionObserverEntry 对象数组。 每个对象都包含更新过的交点数据针对你所观测的元素之一。
[IntersectionObserverEntry] 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: div#observee // ...
rootBounds 是在根元素上调用 getBoundingClientRect()
的结果,默认就是可见窗口。boundingClientRect 是在被观测元素上调用 getBoundingClientRect
的结果。intersectionRect 是这两个矩形的交界,它告诉你被观测的哪块区域是可见的。intersectionRatio 把两个紧紧关联起来,告诉你元素有多少可见。 有这些信息供你使用,你就能非常高效地实现一些像即时加载资源等功能。
IntersectionObserver 总是异步传输这些数据,你的回调函数代码会在主线程运行。
另外,规范明确指出 IntersectionObserver 实现应该使用requestIdleCallback()。 这意味着调用你注册的回调函数的优先级较低,浏览器在空闲时间才会这样做。 这是一个有意识的设计决定。
滚动 div
我不是很喜欢在某个元素里滚动,但我不会再这里和你争,IntersectionObserver 也不是。 当你不是在滚动整个可见窗口时,options 对象可带一个 root 参数用于指定滚动的元素。 有一点很重要你需要牢记:root
需要是所有被观测元素的直接或间接父级。
检测所有东西!
不能这样!作为一个开发者写出这样的代码是不称职的。 这种用法对用户的 CPU 非常不友好。 考虑一个无线滚动的例子,在这种情况下,更为可取的方案是在 DOM 添加岗哨,观测(并且复用)他们。 你应该在无线滚动区域的最后一个元素之后添加一个岗哨。 当那个岗哨进入可见窗口时,你就可以在回调函数里加载数据,创建后面的元素,添加到 DOM 里,并且随之更新岗哨的位置。 如果你正确的复用了这些岗哨,就无需在调用 observe()
。 IntersectionObserver 扔可以继续工作。
Moar Updates, Please
如同之前所说,当被观测的元素部分进入可见窗口时会触发回调函数一次,当它离开可见窗口时会触发另一次。 这样就回答了一个问题:元素 X 在不在可见窗口里。 但在某些场合,仅仅如此还不够。
就就轮到 threshold 起作用了。 它允许你定义一个 intersectionRatio 临界值。 每次 intersectionRatio
经过这些值的时候,你的回调函数都会被调用。threshold
的默认值是[0]
,就是默认行为。 如果我们把 threshold
改为 [0, 0.25, 0.5, 0.75, 1]
,当元素的每四分之一变为可见时,我们都会收到通知:
还有其他别的属性吗?
到目前为止,仅剩一个属性没在上文列出。 rootMargin 允许你指定到跟元素的距离,允许你有效的扩大或缩小交叉区域面积。 这些 margin 使用 CSS 风格的字符串,例如 10px 20px 30px 40px
,依次指定上、右、下、左边距。 总结一下,IntersectionObservers
结构提供了如下选项:
new IntersectionObserver(entries => {/* … */}, { // 用于计算相交区域的根元素 // 如果未提供,使用最上级文档的可见窗口 root: null, // 同 margin,可以是 1、2、3、4 个值,允许时负值。 // 如果显式指定了跟元素,该值可以使用百分比,即根元素大小的百分之多少。 // 如果没指定根元素,使用百分比会出错。 rootMargin: "0px", // 触发回调函数的临界值,用 0 ~ 1 的比率指定,也可以是一个数组。 // 其值是被观测元素可视面积 / 总面积。 // 当可视比率经过这个值的时候,回调函数就会被调用。 threshold: [0], });
iframe 魔法
设计 IntersectionObserver 的时候,我们着重考虑了广告服务和社交网络组件的需要,他们经常会内嵌在 iframe 里。使用 IntersectionObserver 可以简单的知道他们是否可见。 如果 iframe 在观测它内部的某个元素,滚动 iframe 本身或者滚动 iframe 外层的窗口都会在何时的时间触发回调函数。 在后一种情况,rootBounds
应该被设置为 null
以防止跨域泄漏数据。
IntersectionObserver 不是干什么的
有一点要记住:IntersectionObserver 不是完美精确到像素级别,也不是低延时性的。 使用它实现类似依赖滚动效果的动画注定会失败,因为回调函数被调用的时候那些数据——严格来说——已经过期了。 这篇说明 有更多IntersectionObserver
的用法细节。
我在回调函数中可以做多少工作?
简单的说:在回调函数中花大量时间会让你的 app 卡顿。所有的最佳实践在这里都适用。
去使用它吧
浏览器对于 IntersectionObserver
的支持度仍然较低,它不会再每个地方都正常工作。 在此期间,WICG 正在给它开发 polyfill。 当然,你没法获得原生实现给你的性能提升。