一起認(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)注微信公眾號 :前端印象