瀏覽器的五種 Observer,你用過幾種?
網(wǎng)頁開發(fā)中我們經(jīng)常要處理用戶交互,我們會(huì)用 addEventListener 添加事件監(jiān)聽器來監(jiān)聽各種用戶操作,比如 click、mousedown、mousemove、input 等,這些都是由用戶直接觸發(fā)的事件。
那么對(duì)于一些不是由用戶直接觸發(fā)的事件呢?比如元素從不可見到可見、元素大小的改變、元素的屬性和子節(jié)點(diǎn)的修改等,這類事件如何監(jiān)聽呢?
瀏覽器提供了 5 種 Observer 來監(jiān)聽這些變動(dòng):MutationObserver、IntersectionObserver、PerformanceObserver、ResizeObserver、ReportingObserver。
我們分別來看一下:
IntersectionObserver
一個(gè)元素從不可見到可見,從可見到不可見,這種變化如何監(jiān)聽呢?
用 IntersectionObserver。
IntersectionObserver 可以監(jiān)聽一個(gè)元素和可視區(qū)域相交部分的比例,然后在可視比例達(dá)到某個(gè)閾值的時(shí)候觸發(fā)回調(diào)。
我們準(zhǔn)備兩個(gè)元素:
<div id="box1">BOX111</div>
<div id="box2">BOX222</div>
加上樣式:
#box1,#box2 {
width: 100px;
height: 100px;
background: blue;
color: #fff;
position: relative;
}
#box1 {
top: 500px;
}
#box2 {
top: 800px;
}
這兩個(gè)元素分別在 500 和 800 px 的高度,我們監(jiān)聽它們的可見性的改變。
const intersectionObserver = new IntersectionObserver(
function (entries) {
console.log('info:');
entries.forEach(item => {
console.log(item.target, item.intersectionRatio)
})
}, {
threshold: [0.5, 1]
});
intersectionObserver.observe( document.querySelector('#box1'));
intersectionObserver.observe( document.querySelector('#box2'));
創(chuàng)建一個(gè) IntersectionObserver 對(duì)象,監(jiān)聽 box1 和 box2 兩個(gè)元素,當(dāng)可見比例達(dá)到 0.5 和 1 的時(shí)候觸發(fā)回調(diào)。
瀏覽器跑一下:
可以看到元素 box1 和 box2 在可視范圍達(dá)到一半(0.5)和全部(1)的時(shí)候分別觸發(fā)了回調(diào)。
這有啥用?
這太有用了,我們?cè)谧鲆恍?shù)據(jù)采集的時(shí)候,希望知道某個(gè)元素是否是可見的,什么時(shí)候可見的,就可以用這個(gè) api 來監(jiān)聽,還有做圖片的懶加載的時(shí)候,可以當(dāng)可視比例達(dá)到某個(gè)比例再觸發(fā)加載。
除了可以監(jiān)聽元素可見性,還可以監(jiān)聽元素的屬性和子節(jié)點(diǎn)的改變:
MutationObserver
監(jiān)聽一個(gè)普通 JS 對(duì)象的變化,我們會(huì)用 Object.defineProperty 或者 Proxy:
而監(jiān)聽元素的屬性和子節(jié)點(diǎn)的變化,我們可以用 MutationObserver:
MutationObserver 可以監(jiān)聽對(duì)元素的屬性的修改、對(duì)它的子節(jié)點(diǎn)的增刪改。
我們準(zhǔn)備這樣一個(gè)盒子:
<div id="box"><button>光</button></div>
加上樣式:
#box {
width: 100px;
height: 100px;
background: blue;
position: relative;
}
就是這樣的:
我們定時(shí)對(duì)它做下修改:
setTimeout(() => {
box.style.background = 'red';
},2000);
setTimeout(() => {
const dom = document.createElement('button');
dom.textContent = '東東東';
box.appendChild(dom);
},3000);
setTimeout(() => {
document.querySelectorAll('button')[0].remove();
},5000);
2s 的時(shí)候修改背景顏色為紅色,3s 的時(shí)候添加一個(gè) button 的子元素,5s 的時(shí)候刪除第一個(gè) button。
然后監(jiān)聽它的變化:
const mutationObserver = new MutationObserver((mutationsList) => {
console.log(mutationsList)
});
mutationObserver.observe(box, {
attributes: true,
childList: true
});
創(chuàng)建一個(gè) MutationObserver 對(duì)象,監(jiān)聽這個(gè)盒子的屬性和子節(jié)點(diǎn)的變化。
瀏覽器跑一下:
可以看到在三次變化的時(shí)候都監(jiān)聽到了并打印了一些信息:
第一次改變的是 attributes,屬性是 style:
第二次改變的是 childList,添加了一個(gè)節(jié)點(diǎn):
第三次也是改變的 childList,刪除了一個(gè)節(jié)點(diǎn):
都監(jiān)聽到了!
這個(gè)可以用來做什么呢?比如文章水印被人通過 devtools 去掉了,那么就可以通過 MutationObserver 監(jiān)聽這個(gè)變化,然后重新加上,讓水印去不掉。
當(dāng)然,還有很多別的用途,這里只是介紹功能。
除了監(jiān)聽元素的可見性、屬性和子節(jié)點(diǎn)的變化,還可以監(jiān)聽大小變化:
ResizeObserver
窗口我們可以用 addEventListener 監(jiān)聽 resize 事件,那元素呢?
元素可以用 ResizeObserver 監(jiān)聽大小的改變,當(dāng) width、height 被修改時(shí)會(huì)觸發(fā)回調(diào)。
我們準(zhǔn)備這樣一個(gè)元素:
<div id="box"></div>
添加樣式:
#box {
width: 100px;
height: 100px;
background: blue;
}
在 2s 的時(shí)候修改它的高度:
const box = document.querySelector('#box');
setTimeout(() => {
box.style.width = '200px';
}, 3000);
然后我們用 ResizeObserver 監(jiān)聽它的變化:
const resizeObserver = new ResizeObserver(entries => {
console.log('當(dāng)前大小', entries)
});
resizeObserver.observe(box);
在瀏覽器跑一下:
大小變化被監(jiān)聽到了,看下打印的信息:
可以拿到元素和它的位置、尺寸。
這樣我們就實(shí)現(xiàn)了對(duì)元素的 resize 的監(jiān)聽。
除了元素的大小、可見性、屬性子節(jié)點(diǎn)等變化的監(jiān)聽外,還支持對(duì) performance 錄制行為的監(jiān)聽:
PerformanceObserver
瀏覽器提供了 performance 的 api 用于記錄一些時(shí)間點(diǎn)、某個(gè)時(shí)間段、資源加載的耗時(shí)等。
我們希望記錄了 performance 那就馬上上報(bào),可是怎么知道啥時(shí)候會(huì)記錄 performance 數(shù)據(jù)呢?
用 PeformanceObserver。
PerformanceObserver 用于監(jiān)聽記錄 performance 數(shù)據(jù)的行為,一旦記錄了就會(huì)觸發(fā)回調(diào),這樣我們就可以在回調(diào)里把這些數(shù)據(jù)上報(bào)。
比如 performance 可以用 mark 方法記錄某個(gè)時(shí)間點(diǎn):
performance.mark('registered-observer');
用 measure 方法記錄某個(gè)時(shí)間段:
performance.measure('button clicked', 'from', 'to');
后兩個(gè)個(gè)參數(shù)是時(shí)間點(diǎn),不傳代表從開始到現(xiàn)在。
我們可以用 PerformanceObserver 監(jiān)聽它們:
<html>
<body>
<button onclick="measureClick()">Measure</button>
<img src="https://p9-passport.byteacctimg.com/img/user-avatar/4e9e751e2b32fb8afbbf559a296ccbf2~300x300.image" />
<script>
const performanceObserver = new PerformanceObserver(list => {
list.getEntries().forEach(entry => {
console.log(entry);// 上報(bào)
})
});
performanceObserver.observe({entryTypes: ['resource', 'mark', 'measure']});
performance.mark('registered-observer');
function measureClick() {
performance.measure('button clicked');
}
</script>
</body>
</html>
創(chuàng)建 PerformanceObserver 對(duì)象,監(jiān)聽 mark(時(shí)間點(diǎn))、measure(時(shí)間段)、resource(資源加載耗時(shí)) 這三種記錄時(shí)間的行為。
然后我們用 mark 記錄了某個(gè)時(shí)間點(diǎn),點(diǎn)擊 button 的時(shí)候用 measure 記錄了某個(gè)時(shí)間段的數(shù)據(jù),還加載了一個(gè)圖片。
當(dāng)這些記錄行為發(fā)生的時(shí)候,希望能觸發(fā)回調(diào),在里面可以上報(bào)。
我們?cè)跒g覽器跑一下試試:
可以看到 mark 的時(shí)間點(diǎn)記錄、資源加載的耗時(shí)、點(diǎn)擊按鈕的 measure 時(shí)間段記錄都監(jiān)聽到了。
分別打印了這三種記錄行為的數(shù)據(jù):
mark:
圖片加載:
measure:
用了這些數(shù)據(jù),就可以上報(bào)上去做性能分析了。
除了元素、performance 外,瀏覽器還有一個(gè) reporting 的監(jiān)聽:
ReportingObserver
當(dāng)瀏覽器運(yùn)行到過時(shí)(deprecation)的 api 的時(shí)候,會(huì)在控制臺(tái)打印一個(gè)過時(shí)的報(bào)告:
瀏覽器還會(huì)在一些情況下對(duì)網(wǎng)頁行為做一些干預(yù)(intervention),比如會(huì)把占用 cpu 太多的廣告的 iframe 刪掉:
會(huì)在網(wǎng)絡(luò)比較慢的時(shí)候把圖片替換為占位圖片,點(diǎn)擊才會(huì)加載:
這些干預(yù)都是瀏覽器做的,會(huì)在控制臺(tái)打印一個(gè)報(bào)告:
這些干預(yù)或者過時(shí)的 api 并不是報(bào)錯(cuò),所以不能用錯(cuò)誤監(jiān)聽的方式來拿到,但這些情況對(duì)網(wǎng)頁 app 來說可能也是很重要的:
比如我這個(gè)網(wǎng)頁就是為了展示廣告的,但瀏覽器一干預(yù)給我把廣告刪掉了,我卻不知道。如果我知道的話或許可以優(yōu)化下 iframe。
比如我這個(gè)網(wǎng)頁的圖片很重要,結(jié)果瀏覽器一干預(yù)給我換成占位圖了,我卻不知道。如果我知道的話可能會(huì)優(yōu)化下圖片大小。
所以自然也要監(jiān)聽,所以瀏覽器提供了 ReportingObserver 的 api 用來監(jiān)聽這些報(bào)告的打印,我們可以拿到這些報(bào)告然后上傳。
const reportingObserver = new ReportingObserver((reports, observer) => {
for (const report of reports) {
console.log(report.body);//上報(bào)
}
}, {types: ['intervention', 'deprecation']});
reportingObserver.observe();
ReportingObserver 可以監(jiān)聽過時(shí)的 api、瀏覽器干預(yù)等報(bào)告等的打印,在回調(diào)里上報(bào),這些是錯(cuò)誤監(jiān)聽無法監(jiān)聽到但對(duì)了解網(wǎng)頁運(yùn)行情況很有用的數(shù)據(jù)。
文中的代碼上傳到了 github:https://github.com/QuarkGluonPlasma/browser-api-exercize
總結(jié)
監(jiān)聽用戶的交互行為,我們會(huì)用 addEventListener 來監(jiān)聽 click、mousedown、keydown、input 等事件,但對(duì)于元素的變化、performance 的記錄、瀏覽器干預(yù)行為這些不是用戶交互的事件就要用 XxxObserver 的 api 了。
瀏覽器提供了這 5 種 Observer:
- IntersectionObserver:監(jiān)聽元素可見性變化,常用來做元素顯示的數(shù)據(jù)采集、圖片的懶加載
- MutationObserver:監(jiān)聽元素屬性和子節(jié)點(diǎn)變化,比如可以用來做去不掉的水印
- ResizeObserver:監(jiān)聽元素大小變化
- 還有兩個(gè)與元素?zé)o關(guān)的:
- PerformanceObserver:監(jiān)聽 performance 記錄的行為,來上報(bào)數(shù)據(jù)
- ReportingObserver:監(jiān)聽過時(shí)的 api、瀏覽器的一些干預(yù)行為的報(bào)告,可以讓我們更全面的了解網(wǎng)頁 app 的運(yùn)行情況
這些 api 相比 addEventListener 添加的交互事件來說用的比較少,但是在特定場(chǎng)景下都是很有用的。
瀏覽器的 5 種 Observer,你用過幾種呢?在什么情況下用到過呢?