一起認(rèn)識下瀏覽器的 5 種觀察器 API

來源:大轉(zhuǎn)轉(zhuǎn)FE

“圖片懶加載”,這個詞語想必大家再熟悉不過了。傳統(tǒng)的實現(xiàn)方法是,監(jiān)聽scroll事件,獲取img元素相對于視口的頂點位置el.getBoundingClientRect().top,只要這個值小于瀏覽器的高度window.innerHeight就說明進入可視區(qū)域,當(dāng)圖片進入可視區(qū)域時再去加載圖片資源。

這種方法的缺點是,由于scroll事件密集發(fā)生,計算量很大,容易造成性能問題。

目前瀏覽器API中的 IntersectionObserver 交叉觀察器,可自動"觀察"目標(biāo)元素與根元素的交叉區(qū)域的變化,以此判斷元素是否可見。利用這個方法,在觀察到元素可見時,再去加載圖片資源。這樣“圖片懶加載”實現(xiàn)起來就很容易了。

當(dāng)然瀏覽器的觀察器,不僅IntersectionObserver這一種。下面我們依次介紹下瀏覽器5種觀察器的基本用法,以及它們的應(yīng)用。

首先來看一下,什么是瀏覽器的觀察器?

一、什么是瀏覽器的觀察器?
針對一些不是由用戶直接觸發(fā)的事件,比如DOM元素從不可見到可見、DOM大小、屬性的改變和子節(jié)點個數(shù)的修改等,瀏覽器提供特定的api去監(jiān)控這些變化,這些api就是瀏覽器的觀察器。

二、瀏覽器的觀察器有哪些?
瀏覽器的觀察器共有 5 種 :IntersectionObserver(交叉觀察器)、MutationObserver(變化觀察器)、ResizeObserver(大小觀察器)、PerformanceObserver(性能觀察器)、ReportingObserver(報告觀察器) 。

2.1 IntersectionObserver 交叉觀察器
該觀察器自動"觀察"目標(biāo)元素與根元素交叉區(qū)域的變化。默認(rèn)根元素為文檔視口,此時交叉區(qū)域的變化決定了用戶在當(dāng)前視口能否看到目標(biāo)元素,因此它經(jīng)常被用于“元素可見性”觀察。比如:圖片懶加載、無限滾動、廣告曝光量統(tǒng)計等。



上圖中,目標(biāo)元素粉色方塊不僅會隨著窗口滾動,還會在容器Box1里面滾動,目標(biāo)元素與視口(或指定的根元素)產(chǎn)生的交叉區(qū)域會不斷變化。我們將這個交叉區(qū)域占目標(biāo)元素的比例,稱為目標(biāo)元素的交叉比例intersectionRatio。

注意:根元素為視口時(左圖),交叉比例大于0,即元素可見,交叉比例等于0,即元素不可見。指定其他元素為根元素時(右圖),根元素必須是目標(biāo)元素的祖先節(jié)點,此時交叉比例大于0不一定代表元素在當(dāng)前視口可見。

2.1.1 基本用法
通過new IntersectionObserver(callback[, options]) 創(chuàng)建觀察器實例 observer,并按照options配置,指定根元素root、根元素的外邊距rootMargin、執(zhí)行callback的交叉比例的閾值threshold。
let options = { //配置observer實例的對象
    // root: document.querySelector('#parentBox'), // 指定根元素,必須是目標(biāo)元素的父級元素; 默認(rèn):文檔視口
    // rootMargin: "0px 0px 0px 0px", //根元素的外邊距。類似于 CSS 中的 margin 屬性。默認(rèn)值是"0px 0px 0px 0px",分別表示 top、right、bottom 和 left 四個方向的值,用來擴展或縮小rootBounds這個矩形的大小,從而影響intersectionRect交叉區(qū)域的大小。
    threshold: [0] //目標(biāo)元素和根元素相交部分的比例達到該值的時候,callback 函數(shù)將會被執(zhí)行,eg: 1 、[0.5 , 1],當(dāng)為數(shù)組時每達到該值都會執(zhí)行 callback 函數(shù)。默認(rèn)值為[0]。
}
let observer = new IntersectionObserver(callback, options);
定義觀察到目標(biāo)元素與根元素交叉區(qū)域變化時的回調(diào)函數(shù)callback(entries, observer)。entries數(shù)組中,每個成員都是一個IntersectionObserverEntry對象,如果同時有兩個被觀察的對象的可見性發(fā)生變化,entries數(shù)組就會有兩個成員。一般會觸發(fā)兩次callback。一次是目標(biāo)元素剛剛進入視口(開始可見),另一次是完全離開視口(開始不可見)。
let callback = (entries, observer) => {
    entries.forEach(entry => {
        consloe.log(entry) //包含目標(biāo)元素的信息的對象
        // entry.time:可見性發(fā)生變化的時間,是一個高精度時間戳,單位為毫秒
        // entry.target:被觀察的目標(biāo)元素,是一個 DOM 節(jié)點對象
        // entry.rootBounds:根元素的矩形區(qū)域的信息,getBoundingClientRect()方法的返回值,如果沒有根元素(即直接相對于視口滾動),則返回null
        // entry.boundingClientRect:目標(biāo)元素的矩形區(qū)域的信息
        // entry.intersectionRect:目標(biāo)元素與視口(或根元素)的交叉區(qū)域的信息
        // entry.intersectionRatio:根和目標(biāo)元素的交叉區(qū)域的比例值,即intersectionRect占boundingClientRect的比例,0 為完全不可見,1 為完全可見
        // entry.isIntersecting:true表示從不可視狀態(tài)變?yōu)榭梢暊顟B(tài)。false表示從可視狀態(tài)到不可視狀態(tài):false
    });
};
observer.observe(targetNode) 指定目標(biāo)元素 targetNode1、targetNode2,開始觀察。
//observe的參數(shù)是一個 DOM 節(jié)點對象。如果要觀察多個節(jié)點,就要多次調(diào)用這個方法。
observer.observe(targetNode1);
observer.observe(targetNode2); //開始觀察目標(biāo)元素。
// observer.disconnect(); //關(guān)閉觀察器。
// observer.takeRecords(); //返回所有觀察目標(biāo)對象數(shù)組。
// observer.unobserve(targetNode1); //停止觀察特定目標(biāo)元素。
2.1.2 實例:圖片懶加載
創(chuàng)建交叉觀察器,通過observe為所有的圖片資源img開啟了交叉觀察,當(dāng)某個圖片資源,從不可視狀態(tài)變?yōu)榭梢暊顟B(tài)時,便添加圖片的src屬性,從而引發(fā)圖片資源的加載。

const observer = new IntersectionObserver((entries, observer) => {
    entries.forEach(entry => {
        if (entry.isIntersecting) { //true表示從不可視狀態(tài)變?yōu)榭梢暊顟B(tài)
            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); //新數(shù)據(jù)追加在footerSentinel之前
    })
});

// 開始觀察
observer.observe(
  document.querySelector('.footerSentinel')
);
2.1.4 實例:網(wǎng)頁廣告的曝光量統(tǒng)計
很多時候,廣告圖片不一定需要全部展示才算被用戶看到,有時候圖片只展示了60%時,主要信息已經(jīng)被用戶看到,這種情況其實是可以算作一次曝光量的統(tǒng)計。為了實現(xiàn)這種廣告的曝光量的精確統(tǒng)計,我們可以創(chuàng)建交叉管理器,觀察到廣告目標(biāo)元素的交叉比例intersectionRatio達到0.6時,判定廣告的曝光量+1

 const intersectionObserver = new IntersectionObserver((entries)=> {
     entries.forEach(entry => {
         if(entry.intersectionRatio > 0){
            console.log('info:');
            console.log('廣告位元素和可視區(qū)域相交部分的比例:'+entry.intersectionRatio + ',廣告曝光量?1')
            intersectionObserver.unobserve(document.querySelector('.DemoIntersectionObserver .ad'))
         }
     })
 },{
     threshold: 0.6,
 });
intersectionObserver.observe(document.querySelector('.DemoIntersectionObserver .ad'));
看一下實際效果



2.2 MutationObserver 變化觀察器
該觀察器"觀察"目標(biāo)元素屬性和子節(jié)點的變化。目標(biāo)元素DOM發(fā)生變動就會觸發(fā)觀察器的回調(diào)函數(shù)。注意:異步觸發(fā),DOM的變動并不會馬上觸發(fā),而是要等到當(dāng)前所有DOM操作都結(jié)束才觸發(fā)。

2.2.1 基本用法
定義觀察到目標(biāo)元素的特定變動時的回調(diào)函數(shù)callback(mutationList, observer)回調(diào)函數(shù),mutationList為包含目標(biāo)元素DOM變化相關(guān)信息的對象的數(shù)組,數(shù)組中,每個成員都是一個MutationRecord對象
let callback = (mutationList, observer)  => {
    mutationList.forEach((mutation) => {
    console.log(mutation.target) //發(fā)生變動的DOM節(jié)點
    console.log(mutation.previousSibling) //前一個同級節(jié)點,如果沒有則返回null。
    console.log(mutation.nextSibling) //下一個同級節(jié)點,如果沒有則返回null。
    switch(mutation.type) {//目標(biāo)元素變化類型'childList' || 'attributes' | 'characterData'
        case 'childList':
            console.log(mutation.addedNodes) //新增的 DOM 節(jié)點
            console.log(mutation.removedNodes) //刪除的 DOM 節(jié)點
            break;
        case 'attributes':
            console.log(mutation.attributeName) //被更改的屬性名稱,如果設(shè)置了attributeFilter,則只返回預(yù)先指定的屬性。
            console.log(mutation.oldValue) //該屬性之前的值,這個屬性只對attribute和characterData變動有效,如果發(fā)生childList變動,則返回null。
            break;
        }
    });
};





通過new MutationObserver(callback) 創(chuàng)建觀察器實例 observer
let observer = new MutationObserver(callback);
observer.observe(targetNode[, options]), 按照options配置指定要觀察的特定變動,并開始觀察目標(biāo)元素 targetNode。其中:childList,attributes、characterData 三個DOM變動類型的屬性之中,至少有一個必須為true,若均未指定將報錯。
let targetNode = document.querySelector("#someElement");
let options = {
    childList: true,  //  DOM 變動類型:是否觀察目標(biāo)子節(jié)點添加或刪除,默認(rèn)為false。
    attributes: true, //  DOM 變動類型:是否觀察目標(biāo)節(jié)點屬性變動,默認(rèn)為false。
    characterData: false, //  DOM 變動類型:是否觀察文本節(jié)點變化。無默認(rèn)值
    subtree: true, // 是否觀察后代節(jié)點,默認(rèn)為false。
    //注意:childList,attributes、characterData 三個屬性之中,至少有一個必須為 true
    // attributeOldValue: true, //表示觀察attributes變動時,是否需要記錄變動前的屬性值。
    // characterDataOldValue: true, //表示觀察characterData變動時,是否需要記錄變動前的值。
    // attributeFilter: ['class','src'], //數(shù)組,表示需要觀察的特定屬性(比如['class','src'])。
}
observer.observe(targetNode, options); //開始觀察目標(biāo)元素,按照options配置指定所要觀察的特定變動。
// observer.disconnect(); //停止觀察。
// observer.takeRecords(); //返回所有觀察目標(biāo)對象數(shù)組。
2.3 ResizeObserver 大小觀察器 (實驗)
該觀察器"觀察"Element內(nèi)容區(qū)域的改變或SVGElement的邊界框的改變,每次元素內(nèi)容或邊框的大小變化時都會向觀察者傳遞通知。

2.3.1 基本用法
定義觀察到目標(biāo)元素的大小變化的回調(diào)函數(shù)callback(entries, observer),entries為包含目標(biāo)元素大小變化的相關(guān)信息的數(shù)組,數(shù)組中,每個成員都是一個ResizeObserverEntry對象
let callback = (entries, observer) => {
    entries.forEach(entry => {
        console.log('當(dāng)前大小', `${entry.contentRect.width} x ${entry.contentRect.height}`)
        /**
         * entry.target :目標(biāo)元素
         * entry.borderBoxSize: 包含目標(biāo)元素的新邊框框大小的對象
         * entry.contentBoxSize: 包含目標(biāo)元素的新內(nèi)容框大小的對象
         * entry.contentRect: 包含目標(biāo)元素的新大小的對象
         * entry.devicePixelContentBoxSize 包含目標(biāo)元素以設(shè)備像素為單位的新內(nèi)容框大小的對象
         * */
    });
}
通過new ResizeObserver(callback) 創(chuàng)建觀察器實例 observer
let observer = new ResizeObserver(callback);
observer.observe(targetNode); 開始觀察目標(biāo)元素 targetNode。
let targetNode = document.querySelector("#someElement");
observer.observe(targetNode);//開始觀察目標(biāo)元素
// observer.disconnect(); //停止觀察。
// observer.takeRecords(targetNode); //停止觀察目標(biāo)元素。
2.4 PerformanceObserver 性能觀察器
該觀察器用于“觀察”記錄 performance 數(shù)據(jù)的行為,一旦記錄了就會觸發(fā)回調(diào),可以在回調(diào)里上報這些性能相關(guān)的數(shù)據(jù)。

瀏覽器API performance 用于記錄一些時間點、某個時間段、資源加載的耗時等;附上:performance詳細(xì)用法

2.4.1 基本用法
定義觀察到目標(biāo)元素的特定變動時的回調(diào)函數(shù)callback(list, observer)。list.getEntries()為包含options中指定的相關(guān)performance數(shù)據(jù)的對象的數(shù)組,每個成員都是一個PerformanceEntry對象
let callback = (list, observer) => {
    list.getEntries().forEach(entry => {
        console.log(entry); //entry為按startTime排序的performance上報的數(shù)據(jù)對象,自動根據(jù)所請求資源的變化而改變,也可以用mark(),measure()方法自定義添加
        /**
         * entry.name:資源名稱,是資源的絕對路徑或調(diào)用mark方法自定義的名稱
         * entry.entryType:資源類型,entryType類型不同數(shù)組中的對象結(jié)構(gòu)也不同
         * entry.startTime:開始時間
         * entry.duration:加載時間
         * entry.entryType == 'paint' && entry.name == 'first-paint':'首次繪制,繪制Body',
         * entry.entryType == 'paint' && entry.name == 'first-contentful-paint':'首次有內(nèi)容的繪制,第一個dom元素繪制完成',
         * entry.entryType == 'paint' && entry.name == 'first-meaningful-paint':'首次有意義的繪制',
        */
    });
}
通過new PerformanceObserver(callback) 創(chuàng)建觀察器實例 observer
const observer = new PerformanceObserver(callback)
observer.observe(options), 按照options配置,指定所要觀察的performance數(shù)據(jù)相關(guān)變化。
let options = {
    entryTypes:[// 類型為string[],必填,且數(shù)組不能為空,數(shù)組中某個字符串取的值無效,瀏覽器會自動忽略它
        'longtask', // 長任務(wù) (>50ms)
        'frame', // 幀的變化,常用于動畫監(jiān)聽,使用時注意兼容
        'navigation', // 頁面加載||刷新||重定向
        'resource', // 資源加載
        'mark',//  自定義記錄的某個時間點
        'measure',//  自定義記錄的某個時間段
        'paint'//  瀏覽器繪制
    ]
};
observer.observe(options); //當(dāng)記錄的性能指標(biāo)在指定的 entryTypes 之中時,將調(diào)用性能觀察器的回調(diào)函數(shù)。
// observer.disconnect(); //阻止性能觀察器接收任何性能指標(biāo)事件。
// observer.takeRecords(); //返回存儲在性能觀察器中的性能指標(biāo)的列表,并將其清空。
2.4.2 實例:“小雞仔的一生”
下面我們通過”小雞仔的一生“來看一下MutationObserver、ResizeObserver、PerformanceObserver的使用。

首先,它是只會下蛋的母雞,但雞蛋時而被偷。且現(xiàn)在疫情肆虐,小雞可能會發(fā)燒生病,需隨時關(guān)注小雞的健康狀態(tài),及時收雞蛋。等到小雞長大“成熟”,給它賣掉。并記錄下顧客看到小雞的商品圖到真正下單的間隔時長。

首先給小雞搭建好“籠子”,把“小雞”和“雞蛋”放進去

    <div class="Demo">
      <div class="chicken normal">
        <img src='./image/chicken.jpeg' alt="" />
        <div class="egg"></div>
      </div>
    </div>
創(chuàng)建MutationObserver觀察器,觀察小雞的變化,觀察到它的“健康屬性”className變化和“雞蛋”(子節(jié)點)數(shù)量變化時,分別“提醒”小雞健康狀態(tài)和雞蛋個數(shù)變化如下

   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('小雞發(fā)燒了') }
           else{ console.log('小雞健康') }
           break;
       }
   });
   });
   mutationObserver.observe(document.querySelector('.Demo .chicken'),{childList: true,attributes: true,});
創(chuàng)建ResizeObserver觀察器,觀察小雞的大小的變化,當(dāng)小雞“成熟”時,將小雞“賣出”

    const resizeObserver = new ResizeObserver(entries => {
    entries.forEach(entry => {
        if(entry.contentRect.width > 200){
            console.log('小雞賣出!當(dāng)前大小', `${entry.contentRect.width} x ${entry.contentRect.height}`)
        }
        else
            console.log('小雞當(dāng)前大小', `${entry.contentRect.width} x ${entry.contentRect.height}`)
    });
    });
    resizeObserver.observe(document.querySelector(".Demo .chicken img"));
</html>
創(chuàng)建定時器,隨機更新小雞的健康狀態(tài)和雞蛋個數(shù), 并隨機更新小雞的大小

   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);
小雞已經(jīng)“成熟”,停止觀察,并清空定時器,然后展示小雞的商品頁

    setTimeout(()=>{
        clearInterval(timer)
        mutationObserver.disconnect();
        resizeObserver.disconnect();
        //此時,小雞已經(jīng)“成熟”,展示商品頁信息
    },16000)
</html>
小雞的商品頁,初始化查看商品等方法。查看商品時,記錄一個時間點Start-Mark,下單時,記錄一個時間點End-Mark,上報時間Start-End-Send為End-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'); } //上報點擊【查看商品】到點【下單】中間所用的時間
創(chuàng)建PerformanceObserver觀察器,觀察到performance上報的數(shù)據(jù)時,打印對應(yīng)的時間等數(shù)據(jù)信息(這其中就包含了我們記錄的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、瀏覽器的一些干預(yù)行為報告,在回調(diào)里上報

2.5.1 基本用法
通過new ReportingObserver(callback,options)創(chuàng)建觀察器實例, 按照options配置指定所要觀察的report數(shù)據(jù)。觀察到有報告數(shù)據(jù)時,調(diào)取callback(reports, observer)回調(diào)函數(shù),reports為包含options中指定的相關(guān)報告數(shù)據(jù)的對象report的數(shù)組;
let options = {
    types: ['intervention', 'deprecation'] //string[], ??捎弥涤校篸eprecation(觀察使用過時的api)、 intervention(觀察[瀏覽器干預(yù)行為](https://chromestatus.com/features#intervention))
    //buffered: 在觀察者能夠被創(chuàng)建之前生成的報告是否應(yīng)該是可觀察的;可觀察的(true) 或不可觀察的 (false)
};
let callback = (reports, observer) => {
    reports.forEach(report => {
        console.log(report.body);// 返回report正文,包含詳細(xì)的report對象,目前只有兩種body對象(取決于type的返回值)
        // report.type 生成的報告類型,例如deprecation或intervention。
        // report.url 生成報告的文檔的 URL。
    });
}
const observer = new ReportingObserver(callback, options) //創(chuàng)建觀察器實例, 按照`options`配置指定所要觀察的report數(shù)據(jù)。
observer.observe()開始觀察options中指定的報告隊列中收集報告,數(shù)據(jù)上報時調(diào)用回調(diào)函數(shù)
observer.observe(); //開始觀察options中指定的報告隊列中收集報告,數(shù)據(jù)上報時調(diào)用回調(diào)函數(shù) 。
// observer.disconnect(); //阻止之前開始觀察的報告觀察者收集報告。
// observer.takeRecords(); //返回當(dāng)前包含在觀察者報告隊列中的報告列表,并清空隊列。
注意:報告觀察器現(xiàn)在還是試驗性的API,瀏覽器的支持程度還不夠,尤其是Safari瀏覽器完全不支持。其他觀察器相對比較成熟,但也存在部分兼容問題,使用時要視具體情況考慮



三、總結(jié)
通過以上介紹,相信大家對瀏覽器的5種觀察器都有了一定了解。
如果你還知道這些觀察器的其他用法,歡迎評論區(qū)留言。
如果文中有哪些地方寫得不好、不對的地方,歡迎大家批評指正,感謝您的閱讀,今天也是元氣滿滿的一天,一起加油呦!



作者:大轉(zhuǎn)轉(zhuǎn)FE


歡迎關(guān)注微信公眾號 :前端印象