intersectionObserver API 介绍及实践

背景

为了配合项目的一个前端曝光埋点功能,涉及到列表滚动,动态上报曝光行的数据,进行了一个技术调研。
在前端开发工作中,常常需要判断某个元素是否进入了“视口”,一般的做法是监听滚动容器的滚动事件,调用目标元素位置方法,一般有两种方式:

el.offesetTop - document.documentElement.scrollTop <= viewPortHeight 
el.getBoundingClientReact().top <= viewPortHeight

第一种:

function isInViewPortOfOne (el) {
    // viewPortHeight 兼容所有浏览器写法
    const viewPortHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight 
    const offsetTop = el.offsetTop
    const scrollTop = document.documentElement.scrollTop
    const top = offsetTop - scrollTop
    console.log('top', top)
     // 这里有个+100是为了提前加载+ 100
    return top <= viewPortHeight + 100
}

第二种:

function isInViewPortOfTwo (el) {
    const viewPortHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight 
    const top = el.getBoundingClientRect() && el.getBoundingClientRect().top
    console.log('top', top)
    return top  <= viewPortHeight + 100
}

但是这种会造成页面的重排,对性能影响很大。本身社招ATS列表已经非常臃肿,且已经有一些性能问题,所以需要寻找性能更好的API来实现。
常见引起重排属性和方法:

width     height     margin     padding     display     border     position     overflow     clientWidth     clientHeight 
clientTop     clientLeft     offsetWidth     offsetHeight     offsetTop     offsetLeft     scrollWidth     scrollHeight     scrollTop 
scrollLeft     scrollIntoView()     scrollTo()     getComputedStyle()     getBoundingClientRect()     scrollIntoViewIfNeeded()

介绍IntersectionObserver API

目前有一个新的 IntersectionObserver API,可以自动"观察"元素是否可见,Chrome 51+ 已经支持。由于可见(visible)的本质是,目标元素与视口产生一个交叉区,所以这个API叫做"交叉观察器"。

API
//初步用法 
var io = new IntersectionObserver(callback, option);

IntersectionObserver 是浏览器原生提供的构造函数,接受两个参数: callback是可见性变化时的回调函数,option是配置对象
构造函数的返回值是一个观察器实例。实例的observe方法可以指定观察哪个 DOM 节点。
observe的参数是一个 DOM 节点对象。如果要观察多个节点,就要多次调用这个方法。

// 开始观察 
io.observe(document.getElementById('example')); 
// 停止观察 
io.unobserve(element); 
// 关闭观察器 
io.disconnect();
callback
var io = new IntersectionObserver( entries => { console.log(entries); } );

entries参数是一个数组,每个被观察的成员都是一个IntersectionObserverEntry对象,比如观察了两个元素,那么entries的长度就是2
注意: 第一次调用new IntersectionObserver时,callback函数会先调用一次,即使元素未进入可视区域。

IntersectionObserverEntry

IntersectionObserverEntry对象提供目标元素的信息,一共有七个属性
time:可见性发生变化的时间,是一个高精度时间戳,单位为毫秒
target:被观察的目标元素,是一个 DOM节点对象
rootBounds:根元素的矩形区域的信息,getBoundingClientRect()方法的返回值,如果没有根元素(即直接相对于视口滚动),则返回null
boundingClientRect:目标元素的矩形区域的信息
intersectionRect:目标元素与视口(或根元素)的交叉区域的信息
intersectionRatio:目标元素的可见比例,即intersectionRect占boundingClientRect的比例,完全可见时为1,完全不可见时小于等于0
isIntersecting:一个布尔值,指示target元素是已转换为相交状态(true)还是已脱离相交状态(false)

Option

IntersectionObserver构造函数的第二个参数是一个配置对象。它可以设置以下属性。

threshold

threshold属性决定了什么时候触发回调函数。它是一个数组,每个成员都是一个门槛值,默认为[0],即交叉比例(intersectionRatio)达到0时(开始出现、完全隐藏)触发回调函数。
比如threshold设置为[0.5],这个时候当元素第一次进入视口50%本身的面积的时候会调用一次,最后滚动出视口 50% 本身的面积的时候会再调用一次。
这个是为啥?
因为没有取消observer,如果说只想要调用一次,只要在触发之后调用 unobserve 方法取消 observer就可以了。

new IntersectionObserver( 
  entries => {/* ... */}, 
  { 
    threshold: [0, 0.25, 0.5, 0.75, 1] 
  });

用户可以自定义这个数组。比如,[0, 0.25, 0.5, 0.75, 1]就表示当目标元素0%、25%、50%、75%、100%可见时,会触发回调函数。

root、rootMargin

很多时候,目标元素不仅会随着窗口滚动,还会在容器里面滚动(比如在iframe窗口里滚动)。容器内滚动也会影响目标元素的可见性
IntersectionObserver API支持容器内滚动。root属性指定目标元素所在的容器节点(即根元素)。注意,容器元素必须是目标元素的祖先节点。
大白话来说,root 其实就是可以设置以哪个滚动元素为标准,进行视口的计算。

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

var observer = new IntersectionObserver(
  callback,
  opts
);

上面代码中,除了root属性,还有rootMargin属性。后者定义根元素的margin,用来扩展或缩小rootBounds这个矩形的大小,从而影响intersectionRect交叉区域的大小。它使用CSS的定义方法,比如10px 20px 30px 40px,表示 top、right、bottomleft 四个方向的值。 这样设置以后,不管是窗口滚动或者容器内滚动,只要目标元素可见性变化,都会触发观察器。

注意点

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

关于 requestIdleCallback 可参考: https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestIdleCallback

兼容性

https://developer.mozilla.org/zh-CN/docs/Web/API/IntersectionObserver

polyfill

https://www.npmjs.com/package/intersection-observer

实践

实例一:图片懒加载

var url = ''; // 图片链接
var io = new IntersectionObserver(
  entries => { 
    console.log(entries); 
    entries.forEach(item => {
      if(item.isIntersecting) {
        // 正常的图片懒加载设计应该是默认加载一张缩略图,然后等滚动到视口内再替换 src ,这里直接用 innerHTML 方便,只是演示使用
        item.target.innerHTML = `<img height="100%" src="${url}" />`;
        io.unobserve(item.target)
      }
    })
  }, {
    threshold: 1,
    root: document.querySelector('.container'),
  });
const boxList = document.querySelectorAll('.observer-item');
boxList.forEach(item => {
  io.observe(item);
})

实例二:无限滚动

var io = new IntersectionObserver(
  entries => { 
    console.log(entries); 
    entries.forEach(item => {
      if(item.isIntersecting) {
        loadItems();
      }
    })
  }, {
    threshold: 0.3,
    root: document.querySelector('.container'),
  });
io.observe(document.querySelector('.observer-target'));

function loadItems(){ /*加载新的items*/}

无限滚动时,最好在页面底部有一个页尾栏(又称sentinels)。一旦页尾栏可见,就表示用户到达了页面底部,从而加载新的条目放在页尾栏前面。这样做的好处是,不需要再一次调用observe()方法,现有的IntersectionObserver可以保持使用。
实例三:埋点曝光

const boxList = document.querySelectorAll('.mt-table-scroll .mt-table-row');
if(boxList.length <= 0) return;
var io = new IntersectionObserver((entries) =>{
  entries.forEach(item => {
    // 。。。 埋点曝光代码
    if (item.isIntersecting) {
       // do something
      }
      io.unobserve(item.target)
    }
  })
}, {
  root: null,
  threshold: 0.05, // 阀值设为0.05,当只有比例达到0.05时才触发回调函数
})
// observe遍历监听所有box节点
boxList.forEach(box => io.observe(box))
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容