完爆scroll事件,交叉观察器 IntersectionObserver 在千万级PV页面中的应用实践

image

IntersectionObserver 介绍

概念

IntersectionObserver接口(从属于Intersection Observer API)为开发者提供了一种可以异步监听目标元素与其祖先或视窗(viewport)交叉状态的手段。祖先元素与视窗(viewport)被称为根(root)。

功能

网页开发时,常常需要判断某个元素是否进入了"视口"(viewport),即用户能不能看到它,然后执行相应的逻辑。

image

常见的方法是监听scroll事件,调用元素的getBoundingClientRect方法,得到它对应于视口左上角的坐标,再判断是否在视口之内。这种方法的缺点是,由于scroll事件密集发生,计算量很大,容易造成性能问题。

IntersectionObserver是一个新的API,可以自动观察元素是否进入视口。由于可见的本质是,目标元素与视口产生一个交叉区,所以这个API叫做“交叉观察器”。

用法:

let io = new IntersectionObserver(callback, option);

// 开始观察
io.observe(document.getElementById('example'));

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

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

callback

callback方法是被监听元素可见性发生时,执行的回调函数。一般会触发两次,一次是目标元素刚刚进入视口(开始可见),另一次是完全离开视口(开始不可见)。

var io = new IntersectionObserver(
  entries => {
    console.log(entries);
  }
);

entries是一个数组,数组中的每一个元素都是IntersectionObserverEntry对象。特别注意,如果监听的是10个元素,但是只有2个元素可见性发生变化,数组entries的长度就是2,存放的只是可见性发生变化的元素。

IntersectionObserverEntry对象

IntersectionObserverEntry提供被观察元素的信息,有以下七个属性。

  • boundingClientRect 目标元素的矩形信息

  • intersectionRatio 相交区域和目标元素的比例值

  • intersectionRect/boundingClientRect 不可见时小于等于0

  • intersectionRect 目标元素和视窗(根)相交的矩形信息 可以称为相交区域

  • isIntersecting 目标元素当前是否可见 Boolean值 可见为true

  • rootBounds 根元素的矩形信息,没有指定根元素就是当前视窗的矩形信息

  • target 观察的目标元素

  • time 返回一个记录从IntersectionObserver的时间到交叉被触发的时间的时间戳

上面提到的回调执行两次的问题可以通过IntersectionObserverEntry对象的isIntersecting属性解决,通过这个属性判断是否是开始可见。


option 配置对象

主要用于配置IntersectionObserver的一些观察行为属性,主要有以下属性:

1. threshold

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

new IntersectionObserver(
  entries => {/* ... */}, 
  {
    threshold: [0.3, 0.5]
  }
);

如代码中所示,当目标元素30% 或者 50% 可见时,会触发callback回调。

2. root 属性,rootMargin 属性

IntersectionObserver不仅支持视口内滚动,还支持容器内滚动。root属性指定目标元素所在的容器节点(即根元素)。注意,容器元素必须是目标元素的祖先节点。

rootMargin属性用来扩展或缩小rootBounds这个矩形的大小,从而影响intersectionRect交叉区域的大小。它使用CSS的定义方法,比如10px 20px 30px 40px,表示 top、right、bottomleft 四个方向的值。

var opts = { 
  root: document.querySelector('.container'),
  rootMargin: "500px 0px" 
};

var observer = new IntersectionObserver(
  callback,
  opts
);

应用实践

基于这个API高性能的特性,小编在日常的业务中做了如下的实践,使用效果很不错,同时也简化了业务实现逻辑。

  1. 滚动页面,滑动到指定区域,切换TAB栏选中按钮;
  2. 长页面滑动时,页面模块的曝光量的数据统计;

1. 滚动切换TAB

效果如下图:

image

具体实现代码如下:

// index.html

<div class="default-labelSwitch" id="hotList">
    <!-- 选中状态给li 加selected -->
    <ul>
        <li class="selected" data-hash="cashiermodule" ></li>
        <li data-hash="seckillcont"></li>
        <li data-hash="powervalue"></li>
    </ul>
</div>


<div id="cashiermodule" type="lists">
...

</div>

<div id="seckillcont" type="lists">
...

</div>

<div id="powervalue" type="lists">
...

</div>

// 监听页面模块cashiermodule、seckillcont、powervalue是否进入可视区域

require('intersection-observer');

let io = new IntersectionObserver(entries => {
    entries.forEach(item => {
        if(item.isIntersecting && item.intersectionRatio >= 0.7) {
            let target = $(item.target);
            let id = target.attr('id');
            oLis.removeClass('selected');
            $(`[data-hash*="${id}"]`).addClass('selected');
        }
    })
}, {
    threshold: [0.7]
})

let blocks = $('[type*="lists"]');
blocks.forEach(item => {
    io.observe(item);
})

可以看到,实现了模块进入视口区域70%时,就会触发回调,切换tab栏的按钮选中状态。

2. 统计页面模块曝光量

image

具体代码如下:

// index.html

<div block="moduleA">
...

</div>

<div block="moduleB">
...

</div>

<div block="moduleC">
...

</div>
// 统计模块的曝光量

require('intersection-observer');
let block = {
    init: function () {
        let io = new IntersectionObserver(entries => {
            entries.forEach(item => {
                if(item.isIntersecting) {
                    let target = $(item.target);
                    let block = target.attr('block');
                    window.sendPingback(block);
                }
            })
        })
        
        let blocks = $('[block*="module"]');
        blocks.forEach(item => {
            io.observe(item);
        })
        
    }
}

export default block;

从动画效果中我们看到,滑动到某一模块,就会发送对应的统计请求。

从代码中可知,想要统计哪一块的曝光量,只需要在对应的div上加上block属性即可,方便快捷高效。后面打算在这个基础上进行扩展,统计用户在某一模块的停留时间,进而获取用户的感兴趣区域。

注意

  1. 由于IntersectionObserver存在兼容性问题,所以需要引入补丁文件
npm install intersection-observer --save
  1. IntersectionObserver是异步的,不随着目标元素的滚动同步触发。即只有线程空闲下来,才会执行观察器。

引申

IntersectionObserver还可以实现图片的懒加载,由于还没有在实际项目中实践过,这里就不展开说明了,原理和上面两个案例基本相同,只是需要处理图片相关的逻辑。

关注我

扫一扫 关注我的公众号【前端名狮】,更多精彩内容陪伴你!

【前端名狮】
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,463评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,868评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,213评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,666评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,759评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,725评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,716评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,484评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,928评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,233评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,393评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,073评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,718评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,308评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,538评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,338评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,260评论 2 352

推荐阅读更多精彩内容