利用 IntersectionObserver 做曝光埋点
前些天接了个紧急的简单需求,有多简单呢,就是一张背景图,然后上面加上一个悬浮的按钮。果然简单!不过按惯例,不可能这么简单。果然第二天就说说加了个需求,需要在这个背景图底部继续增加两行,每行三个商品,需要统计点击和曝光事件。上报点击事件倒是简单,曝光则相对麻烦点。
正如 mdn 上所说的
过去,相交检测通常要用到事件监听,并且需要频繁调用
Element.getBoundingClientRect()方法以获取相关元素的边界信息。事件监听和调用Element.getBoundingClientRect()都是在主线程上运行,因此频繁触发、调用可能会造成性能问题。这种检测方法极其怪异且不优雅。
因此如今的浏览器提供了 IntersectionObserver 这个 api ,每当被监视的元素进入或者退出另外一个元素时(或者 viewport ),或者两个元素的相交部分大小发生变化时,该回调方法会被触发执行。
兼容性
在了解如何使用之前,作为开发者我们往往比较关心的一个问题是:这个 API 的兼容性如何?
在 can i use 上看到大部分的浏览器是支持的

那有人就问了,如果老板就是要兼容 IE 呢 😂
幸好 IntersectionObserver 提供了 polyfill ,我们只需要按照文档引入polyfill 就好了。简单瞄了一下 polyfill 源码应该就是回退到 getBoundingClientRect 方案。
使用方法
原先的代码是原生 js 写的,后来(惯例)又加了一些 AB 实验等需求,索性改成 vue 写反而更快了,页面结构大致如下所示。
<template>
<div>
<img class="bg-img" :src="bg-img" />
<img
v-for="product in productList"
:key="product.id"
:src="product.src"
class="product-img"
/>
</div>
</template>
第一步,我们需要创建一个 observer
// 声明当观察到元素进出视口时触发的回调函数
const callback = (changes) => {
changes.forEach(function(change) {
// el 即为我们观察的元素
const el = change.target;
// 上报曝光
console.log("exposure", change);
// 取消观察
observer.unobserve(el);
});
}
// IntersectionObserver 可以接收两个参数,第二个参数 opt 可以定义容器(默认为视口)等配置,这里不展开讲,详细可看 mdn 文档
const observer = new IntersectionObserver(callback,opt || {});
第二步,我们需要在页面初始化的时候观察。
mounted() {
[...document.querySelectorAll(".product-img")].forEach(item => {
observer.observe(item);
});
}
大功告成?no.
踩过的坑
实际上如果这么写的话会有几个问题。
首先就是我们一打开页面,就会发现触发所有的了上报曝光事件。这个问题就出在当我们 observe 的时候,就会触发一次回调,即使元素并没有进出视口。那么我们要做的就是判断元素是否在视口中即可,调整 callback
// 声明当观察到元素进出视口时触发的回调函数 const callback = (changes) => { changes.forEach(function(change) { // el 即为我们观察的元素 const el = change.target; if (change.intersectionRatio > 0) { // 上报曝光 console.log("exposure", change); // 取消观察 observer.unobserve(el); } }); } // IntersectionObserver 可以接收两个参数,第二个参数 opt 可以定义容器(默认为视口)等配置,这里不展开讲,详细可看 mdn 文档 const observer = new IntersectionObserver(callback,opt || {});通过 change.intersectionRatio 可以知道元素在视口中交叉的比例是多少,如果大于 0 则证明元素是在视口中,此时才需要曝光。
经过上述的修改,我们打开页面再看,emmm… 还是一开始就把所有的元素曝光都上报了啊。让我们来看看曝光时 change 的一些属性
{ // ...其他一些属性 // 交叉比例 intersectionRatio: 1 // 目标元素的属性 boundingClientRect: { bottom: 0 height: 0 left: 0 right: 375 top: 0 width: 375 x: 0 y: 0 } }可以看到 intersectionRatio = 1 即说明元素确确实实在视口中了,而 boundingClientRect 中的 height 表示当时元素的高度还是 0,再一看 y 也是 0。瞬间想到:mounted 的时候,图片都还没加载好,那么此时页面还没有撑开,自然所有的产品卡片都是在视口中!也就是说我们 observe 的时机不正确。那么正确的时机应该是在页面 onload 之后,而非在组件的 mounted 事件中。
mounted() { window.addEventListener('load', function() { [...document.querySelectorAll(".product-img")].forEach(item => { observer.observe(item); }); }) }至此就可以正常上报曝光。
优点
总结,IntersectionObserver 有如下几个优点:
- 提供的 Api 友好,使用起来简单
- 兼容性不错,并且提供了 polyfill
- 性能方面浏览器做了相关优化
- 适用性广泛,应用场景则包括上述的曝光埋点,图片懒加载,无限滚动等等