写点什么

一起认识下浏览器的 5 种观察器

  • 2022 年 6 月 17 日
  • 本文字数:8941 字

    阅读完需:约 29 分钟

前言

“图片懒加载”,这个词语想必大家再熟悉不过了。传统的实现方法是,监听 scroll 事件,获取 img 元素相对于视口的顶点位置 el.getBoundingClientRect().top,只要这个值小于浏览器的高度 window.innerHeight 就说明进入可视区域,当图片进入可视区域时再去加载图片资源。


这种方法的缺点是,由于 scroll 事件密集发生,计算量很大,容易造成性能问题。


目前浏览器 API 中的 IntersectionObserver 交叉观察器,可自动"观察"目标元素与根元素的交叉区域的变化,以此判断元素是否可见。利用这个方法,在观察到元素可见时,再去加载图片资源。这样“图片懒加载”实现起来就很容易了。


当然浏览器的观察器,不仅 IntersectionObserver 这一种。下面我们依次介绍下浏览器 5 种观察器的基本用法,以及它们的应用。


首先来看一下,什么是浏览器的观察器?

一、什么是浏览器的观察器?

针对一些不是由用户直接触发的事件,比如 DOM 元素从不可见到可见、DOM 大小、属性的改变和子节点个数的修改等,浏览器提供特定的 api 去监控这些变化,这些 api 就是浏览器的观察器。

二、浏览器的观察器有哪些?

浏览器的观察器共有 5 种 :IntersectionObserver(交叉观察器)、MutationObserver(变化观察器)、ResizeObserver(大小观察器)、PerformanceObserver(性能观察器)、ReportingObserver(报告观察器) 。

2.1 IntersectionObserver 交叉观察器

该观察器自动"观察"目标元素与根元素交叉区域的变化。默认根元素为文档视口,此时交叉区域的变化决定了用户在当前视口能否看到目标元素,因此它经常被用于“元素可见性”观察。比如:图片懒加载、无限滚动、广告曝光量统计等。



图 1



图 2


上图中,目标元素粉色方块不仅会随着窗口滚动,还会在容器 Box1 里面滚动,目标元素与视口(或指定的根元素)产生的交叉区域会不断变化。我们将这个交叉区域占目标元素的比例,称为目标元素的交叉比例intersectionRatio


注意:根元素为视口时(图 1),交叉比例大于 0,即元素可见,交叉比例等于 0,即元素不可见。指定其他元素为根元素时(图 2),根元素必须是目标元素的祖先节点,此时交叉比例大于 0 不一定代表元素在当前视口可见。

2.1.1 基本用法

  1. 通过new IntersectionObserver(callback[, options]) 创建观察器实例 observer,并按照options配置,指定根元素root、根元素的外边距rootMargin、执行callback的交叉比例的阀值threshold


let options = { //配置observer实例的对象    // root: document.querySelector('#parentBox'), // 指定根元素,必须是目标元素的父级元素; 默认:文档视口    // rootMargin: "0px 0px 0px 0px", //根元素的外边距。类似于 CSS 中的 margin 属性。默认值是"0px 0px 0px 0px",分别表示 top、right、bottom 和 left 四个方向的值,用来扩展或缩小rootBounds这个矩形的大小,从而影响intersectionRect交叉区域的大小。    threshold: [0] //目标元素和根元素相交部分的比例达到该值的时候,callback 函数将会被执行,eg: 1 、[0.5 , 1],当为数组时每达到该值都会执行 callback 函数。默认值为[0]。}let observer = new IntersectionObserver(callback, options);
复制代码


  1. 定义观察到目标元素与根元素交叉区域变化时的回调函数callback(entries, observer)entries数组中,每个成员都是一个IntersectionObserverEntry对象,如果同时有两个被观察的对象的可见性发生变化,entries数组就会有两个成员。一般会触发两次callback。一次是目标元素刚刚进入视口(开始可见),另一次是完全离开视口(开始不可见)。


let callback = (entries, observer) => {     entries.forEach(entry => {        consloe.log(entry) //包含目标元素的信息的对象        // entry.time:可见性发生变化的时间,是一个高精度时间戳,单位为毫秒        // entry.target:被观察的目标元素,是一个 DOM 节点对象        // entry.rootBounds:根元素的矩形区域的信息,getBoundingClientRect()方法的返回值,如果没有根元素(即直接相对于视口滚动),则返回null        // entry.boundingClientRect:目标元素的矩形区域的信息        // entry.intersectionRect:目标元素与视口(或根元素)的交叉区域的信息        // entry.intersectionRatio:根和目标元素的交叉区域的比例值,即intersectionRect占boundingClientRect的比例,0 为完全不可见,1 为完全可见        // entry.isIntersecting:true表示从不可视状态变为可视状态。false表示从可视状态到不可视状态:false    });};
复制代码


  1. observer.observe(targetNode) 指定目标元素 targetNode1、targetNode2,开始观察。


//observe的参数是一个 DOM 节点对象。如果要观察多个节点,就要多次调用这个方法。observer.observe(targetNode1);observer.observe(targetNode2); //开始观察目标元素。// observer.disconnect(); //关闭观察器。// observer.takeRecords(); //返回所有观察目标对象数组。// observer.unobserve(targetNode1); //停止观察特定目标元素。
复制代码

2.1.2 实例:图片懒加载

创建交叉观察器,通过observe为所有的图片资源 img 开启了交叉观察,当某个图片资源,从不可视状态变为可视状态时,便添加图片的 src 属性,从而引发图片资源的加载。


const observer = new IntersectionObserver((entries, observer) => {     entries.forEach(entry => {        if (entry.isIntersecting) { //true表示从不可视状态变为可视状态            let img = entry.target;            img.setAttribute('src', img.getAttribute('data-src'))             observer.unobserve(img); // 停止观察已开始加载的图片         }     }) }, {}); Array.from(document.querySelectorAll('img')).forEach((item) => {  observer.observe(item)  //观察所有图片资源,开始观察item});
复制代码

2.1.3 实例:无限滚动

无限滚动时,在页面底部加一个footerSentinel元素。一旦footerSentinel可见,就表示页面滚动到了底部,从而加载新的条目放在footerSentinel前面。


const observer = new IntersectionObserver((entries, observer) => {     entries.forEach(entry => {        if (entry.intersectionRatio <= 0) return; // 如果不可见,就返回        let newData = [1,2,3,4,5];        appendChildBeforeFooter(newData); //新数据追加在footerSentinel之前    }) });
// 开始观察observer.observe( document.querySelector('.footerSentinel'));
复制代码

2.1.4 实例:网页广告的曝光量统计

很多时候,广告图片不一定需要全部展示才算被用户看到,有时候图片只展示了 60%时,主要信息已经被用户看到,这种情况其实是可以算作一次曝光量的统计。为了实现这种广告的曝光量的精确统计,我们可以创建交叉管理器,观察到广告目标元素的交叉比例 intersectionRatio 达到 0.6 时,判定广告的曝光量+1


 const intersectionObserver = new IntersectionObserver((entries)=> {     entries.forEach(entry => {         if(entry.intersectionRatio > 0){            console.log('info:');            console.log('广告位元素和可视区域相交部分的比例:'+entry.intersectionRatio + ',广告曝光量➕1')            intersectionObserver.unobserve(document.querySelector('.DemoIntersectionObserver .ad'))         }     }) },{      threshold: 0.6, });intersectionObserver.observe(document.querySelector('.DemoIntersectionObserver .ad'));
复制代码


看一下实际效果


2.2 MutationObserver 变化观察器

该观察器"观察"目标元素属性和子节点的变化。目标元素 DOM 发生变动就会触发观察器的回调函数。注意:异步触发,DOM 的变动并不会马上触发,而是要等到当前所有 DOM 操作都结束才触发。

2.2.1 基本用法

  1. 定义观察到目标元素的特定变动时的回调函数callback(mutationList, observer)回调函数,mutationList为包含目标元素 DOM 变化相关信息的对象的数组,数组中,每个成员都是一个MutationRecord对象


let callback = (mutationList, observer)  => {     mutationList.forEach((mutation) => {    console.log(mutation.target) //发生变动的DOM节点    console.log(mutation.previousSibling) //前一个同级节点,如果没有则返回null。    console.log(mutation.nextSibling) //下一个同级节点,如果没有则返回null。    switch(mutation.type) {//目标元素变化类型'childList' || 'attributes' | 'characterData'        case 'childList':            console.log(mutation.addedNodes) //新增的 DOM 节点            console.log(mutation.removedNodes) //删除的 DOM 节点            break;        case 'attributes':            console.log(mutation.attributeName) //被更改的属性名称,如果设置了attributeFilter,则只返回预先指定的属性。            console.log(mutation.oldValue) //该属性之前的值,这个属性只对attribute和characterData变动有效,如果发生childList变动,则返回null。            break;        }    });};
复制代码


  1. 通过new MutationObserver(callback) 创建观察器实例 observer


let observer = new MutationObserver(callback); 
复制代码


  1. observer.observe(targetNode[, options]), 按照options配置指定要观察的特定变动,并开始观察目标元素 targetNode。其中:childList,attributes、characterData 三个 DOM 变动类型的属性之中,至少有一个必须为 true,若均未指定将报错。


let targetNode = document.querySelector("#someElement");let options = {    childList: true,  //  DOM 变动类型:是否观察目标子节点添加或删除,默认为false。    attributes: true, //  DOM 变动类型:是否观察目标节点属性变动,默认为false。    characterData: false, //  DOM 变动类型:是否观察文本节点变化。无默认值    subtree: true, // 是否观察后代节点,默认为false。    //注意:childList,attributes、characterData 三个属性之中,至少有一个必须为 true    // attributeOldValue: true, //表示观察attributes变动时,是否需要记录变动前的属性值。    // characterDataOldValue: true, //表示观察characterData变动时,是否需要记录变动前的值。    // attributeFilter: ['class','src'], //数组,表示需要观察的特定属性(比如['class','src'])。}observer.observe(targetNode, options); //开始观察目标元素,按照options配置指定所要观察的特定变动。// observer.disconnect(); //停止观察。// observer.takeRecords(); //返回所有观察目标对象数组。
复制代码

2.3 ResizeObserver 大小观察器 (实验)

该观察器"观察"Element 内容区域的改变或 SVGElement 的边界框的改变,每次元素内容或边框的大小变化时都会向观察者传递通知。

2.3.1 基本用法

  1. 定义观察到目标元素的大小变化的回调函数callback(entries, observer)entries为包含目标元素大小变化的相关信息的数组,数组中,每个成员都是一个ResizeObserverEntry对象


let callback = (entries, observer) => {    entries.forEach(entry => {        console.log('当前大小', `${entry.contentRect.width} x ${entry.contentRect.height}`)        /**         * entry.target :目标元素         * entry.borderBoxSize: 包含目标元素的新边框框大小的对象         * entry.contentBoxSize: 包含目标元素的新内容框大小的对象         * entry.contentRect: 包含目标元素的新大小的对象         * entry.devicePixelContentBoxSize 包含目标元素以设备像素为单位的新内容框大小的对象         * */    });}
复制代码


  1. 通过new ResizeObserver(callback) 创建观察器实例 observer


let observer = new ResizeObserver(callback); 
复制代码


  1. observer.observe(targetNode); 开始观察目标元素 targetNode


let targetNode = document.querySelector("#someElement");observer.observe(targetNode);//开始观察目标元素// observer.disconnect(); //停止观察。// observer.takeRecords(targetNode); //停止观察目标元素。
复制代码

2.4 PerformanceObserver 性能观察器

该观察器用于“观察”记录 performance 数据的行为,一旦记录了就会触发回调,可以在回调里上报这些性能相关的数据。


浏览器 API performance 用于记录一些时间点、某个时间段、资源加载的耗时等;附上:performance详细用法

2.4.1 基本用法

  1. 定义观察到目标元素的特定变动时的回调函数callback(list, observer)list.getEntries()为包含 options 中指定的相关performance数据的对象的数组,每个成员都是一个PerformanceEntry对象


let callback = (list, observer) => {    list.getEntries().forEach(entry => {        console.log(entry); //entry为按startTime排序的performance上报的数据对象,自动根据所请求资源的变化而改变,也可以用mark(),measure()方法自定义添加        /**         * entry.name:资源名称,是资源的绝对路径或调用mark方法自定义的名称         * entry.entryType:资源类型,entryType类型不同数组中的对象结构也不同         * entry.startTime:开始时间         * entry.duration:加载时间         * entry.entryType == 'paint' && entry.name == 'first-paint':'首次绘制,绘制Body',         * entry.entryType == 'paint' && entry.name == 'first-contentful-paint':'首次有内容的绘制,第一个dom元素绘制完成',         * entry.entryType == 'paint' && entry.name == 'first-meaningful-paint':'首次有意义的绘制',        */    });}
复制代码


  1. 通过new PerformanceObserver(callback) 创建观察器实例 observer


const observer = new PerformanceObserver(callback) 
复制代码


  1. observer.observe(options), 按照options配置,指定所要观察的performance数据相关变化。


let options = {    entryTypes:[// 类型为string[],必填,且数组不能为空,数组中某个字符串取的值无效,浏览器会自动忽略它        'longtask', // 长任务 (>50ms)        'frame', // 帧的变化,常用于动画监听,使用时注意兼容        'navigation', // 页面加载||刷新||重定向        'resource', // 资源加载        'mark',//  自定义记录的某个时间点        'measure',//  自定义记录的某个时间段        'paint'//  浏览器绘制    ]};observer.observe(options); //当记录的性能指标在指定的 entryTypes 之中时,将调用性能观察器的回调函数。// observer.disconnect(); //阻止性能观察器接收任何性能指标事件。// observer.takeRecords(); //返回存储在性能观察器中的性能指标的列表,并将其清空。
复制代码

2.4.2 实例:“小鸡仔的一生”

下面我们通过”小鸡仔的一生“来看一下 MutationObserver、ResizeObserver、PerformanceObserver 的使用。


首先,它是只会下蛋的母鸡,但鸡蛋时而被偷。且现在疫情肆虐,小鸡可能会发烧生病,需随时关注小鸡的健康状态,及时收鸡蛋。等到小鸡长大“成熟”,给它卖掉。并记录下顾客看到小鸡的商品图到真正下单的间隔时长。


首先给小鸡搭建好“笼子”,把“小鸡”和“鸡蛋”放进去


    <div class="Demo">      <div class="chicken normal">        <img src='./image/chicken.jpeg' alt="" />        <div class="egg"></div>      </div>    </div>
复制代码


创建 MutationObserver 观察器,观察小鸡的变化,观察到它的“健康属性”className变化和“鸡蛋”(子节点)数量变化时,分别“提醒”小鸡健康状态和鸡蛋个数变化如下


   const mutationObserver = new MutationObserver((mutationsList) => {   mutationsList.forEach((mutation) => {       switch(mutation.type) {         case 'childList':             if(mutation.addedNodes.length>0){console.log('小鸡下蛋了') }             else if(mutation.removedNodes.length>0){ console.log('鸡蛋被偷了1个') }             break;         case 'attributes':           if(mutation.target.className.indexOf('hot')>-1){ console.log('小鸡发烧了') }           else{ console.log('小鸡健康') }           break;       }   });   });   mutationObserver.observe(document.querySelector('.Demo .chicken'),{childList: true,attributes: true,});
复制代码


创建 ResizeObserver 观察器,观察小鸡的大小的变化,当小鸡“成熟”时,将小鸡“卖出”


    const resizeObserver = new ResizeObserver(entries => {    entries.forEach(entry => {        if(entry.contentRect.width > 200){            console.log('小鸡卖出!当前大小', `${entry.contentRect.width} x ${entry.contentRect.height}`)        }        else            console.log('小鸡当前大小', `${entry.contentRect.width} x ${entry.contentRect.height}`)    });    });    resizeObserver.observe(document.querySelector(".Demo .chicken img"));</html>
复制代码


创建定时器,随机更新小鸡的健康状态和鸡蛋个数, 并随机更新小鸡的大小


   let timer = setInterval(() => {   let random = Math.ceil(Math.random() * 4);   targetNodeImg?.setAttribute('style',`width:${targetNodeImg.offsetWidth*1.2}px`)   switch (random) {       case 1: targetNodes.className="chicken hot";break;       case 2: targetNodes.className="chicken";break;       case 3: const dom = document.createElement('div'); dom.className = 'egg'; targetNodes.appendChild(dom);targetNodes.appendChild(dom.cloneNode());break;       case 4: document.querySelectorAll('.egg')[0]&&document.querySelectorAll('.egg')[0].remove(); break;       default:break;   }   },2000);
复制代码


小鸡已经“成熟”,停止观察,并清空定时器,然后展示小鸡的商品页


    setTimeout(()=>{        clearInterval(timer)        mutationObserver.disconnect();        resizeObserver.disconnect();        //此时,小鸡已经“成熟”,展示商品页信息    },16000)</html>
复制代码


小鸡的商品页,初始化查看商品等方法。查看商品时,记录一个时间点Start-Mark,下单时,记录一个时间点End-Mark,上报时间Start-End-SendEnd-Mark-Start-Mark


function startMark() { performance.mark('Start-Mark') } //查看商品function endMark() { performance.mark('End-Mark') } //下单function measureClick() { performance.measure('Start-End-Send','Start-Mark','End-Mark'); } //上报点击【查看商品】到点【下单】中间所用的时间
复制代码


创建 PerformanceObserver 观察器,观察到 performance 上报的数据时,打印对应的时间等数据信息(这其中就包含了我们记录的 mark 信息和页面资源加载等信息)。


let callback = (list, observer) => {    list.getEntries().forEach(entry => {      switch (entry.entryType) {        case 'mark':console.log(`(自定义上报-时间点):${entry.name},时刻:  - ${entry.startTime}`);break;        case 'measure':console.log(`(自定义上报-时间段):${entry.name},时间: ${entry.duration} `);break;        default:break;      }      });}const observer = new PerformanceObserver(callback) observer.observe({ entryTypes:['mark','measure'] });
复制代码


小鸡仔“长大”到被“卖出”,效果展示


2.5 ReportingObserver 报告观察器(实验)

该观察器“观察”过时的 api、浏览器的一些干预行为的报告,在回调里上报

2.5.1 基本用法

  1. 通过new ReportingObserver(callback,options)创建观察器实例, 按照options配置指定所要观察的 report 数据。观察到有报告数据时,调取callback(reports, observer)回调函数,reports为包含 options 中指定的相关报告数据的对象report的数组;


let options = {    types: ['intervention', 'deprecation'] //string[], 常可用值有:deprecation(观察使用过时的api)、 intervention(观察[浏览器干预行为](https://chromestatus.com/features#intervention))    //buffered: 在观察者能够被创建之前生成的报告是否应该是可观察的;可观察的(true) 或不可观察的 (false)};let callback = (reports, observer) => {    reports.forEach(report => {        console.log(report.body);// 返回report正文,包含详细的report对象,目前只有两种body对象(取决于type的返回值)        // report.type 生成的报告类型,例如deprecation或intervention。        // report.url 生成报告的文档的 URL。    });}const observer = new ReportingObserver(callback, options) //创建观察器实例, 按照`options`配置指定所要观察的report数据。
复制代码


  1. observer.observe()开始观察 options 中指定的报告队列中收集报告,数据上报时调用回调函数


observer.observe(); //开始观察options中指定的报告队列中收集报告,数据上报时调用回调函数 。// observer.disconnect(); //阻止之前开始观察的报告观察者收集报告。// observer.takeRecords(); //返回当前包含在观察者报告队列中的报告列表,并清空队列。
复制代码


注意:报告观察器现在还是试验性的 API,浏览器的支持程度还不够,尤其是 Safari 浏览器完全不支持。其他观察器相对比较成熟,但也存在部分兼容问题,使用时要视具体情况考虑


三、总结

  • 通过以上介绍,相信大家对浏览器的 5 种观察器都有了一定了解。

  • 如果你还知道这些观察器的其他用法,欢迎评论区留言。

  • 如果文中有哪些地方写得不好、不对的地方,欢迎大家批评指正,感谢您的阅读,今天也是元气满满的一天,一起加油呦!

四、感谢

  1. 浏览器的 5 种 Observer,你用过几种?

  2. IntersectionObserver API 使用教程

  3. MutationObserver 介绍

  4. 通过自定义指令实现前端曝光埋点


转转研发中心及业界小伙伴们的技术学习交流平台,定期分享一线的实战经验及业界前沿的技术话题。关注公众号「转转技术」,各种干货实践,欢迎交流分享~

用户头像

还未添加个人签名 2019.04.30 加入

转转研发中心及业界小伙伴们的技术学习交流平台,定期分享一线的实战经验及业界前沿的技术话题。 关注公众号「转转技术」,各种干货实践,欢迎交流分享~

评论

发布
暂无评论
一起认识下浏览器的5种观察器_JavaScript_转转技术团队_InfoQ写作社区