瀏覽器的五種 Observer,你用過幾種?
網(wǎng)頁開發(fā)中我們經(jīng)常要處理用戶交互,我們會用 addEventListener 添加事件監(jiān)聽器來監(jiān)聽各種用戶操作,比如 click、mousedown、mousemove、input 等,這些都是由用戶直接觸發(fā)的事件。
那么對于一些不是由用戶直接觸發(fā)的事件呢?比如元素從不可見到可見、元素大小的改變、元素的屬性和子節(jié)點的修改等,這類事件如何監(jiān)聽呢?
瀏覽器提供了 5 種 Observer 來監(jiān)聽這些變動:MutationObserver、IntersectionObserver、PerformanceObserver、ResizeObserver、ReportingObserver。
我們分別來看一下:
IntersectionObserver
一個元素從不可見到可見,從可見到不可見,這種變化如何監(jiān)聽呢?
用 IntersectionObserver。
IntersectionObserver 可以監(jiān)聽一個元素和可視區(qū)域相交部分的比例,然后在可視比例達到某個閾值的時候觸發(fā)回調(diào)。
我們準備兩個元素:
<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;
}
這兩個元素分別在 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)建一個 IntersectionObserver 對象,監(jiān)聽 box1 和 box2 兩個元素,當可見比例達到 0.5 和 1 的時候觸發(fā)回調(diào)。
瀏覽器跑一下:

可以看到元素 box1 和 box2 在可視范圍達到一半(0.5)和全部(1)的時候分別觸發(fā)了回調(diào)。
這有啥用?
這太有用了,我們在做一些數(shù)據(jù)采集的時候,希望知道某個元素是否是可見的,什么時候可見的,就可以用這個 api 來監(jiān)聽,還有做圖片的懶加載的時候,可以當可視比例達到某個比例再觸發(fā)加載。
除了可以監(jiān)聽元素可見性,還可以監(jiān)聽元素的屬性和子節(jié)點的改變:
MutationObserver
監(jiān)聽一個普通 JS 對象的變化,我們會用 Object.defineProperty 或者 Proxy:


而監(jiān)聽元素的屬性和子節(jié)點的變化,我們可以用 MutationObserver:
MutationObserver 可以監(jiān)聽對元素的屬性的修改、對它的子節(jié)點的增刪改。
我們準備這樣一個盒子:
<div id="box"><button>光</button></div>
加上樣式:
#box {
width: 100px;
height: 100px;
background: blue;
position: relative;
}
就是這樣的:


我們定時對它做下修改:
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 的時候修改背景顏色為紅色,3s 的時候添加一個 button 的子元素,5s 的時候刪除第一個 button。
然后監(jiān)聽它的變化:
const mutationObserver = new MutationObserver((mutationsList) => {
console.log(mutationsList)
});
mutationObserver.observe(box, {
attributes: true,
childList: true
});
創(chuàng)建一個 MutationObserver 對象,監(jiān)聽這個盒子的屬性和子節(jié)點的變化。
瀏覽器跑一下:

可以看到在三次變化的時候都監(jiān)聽到了并打印了一些信息:
第一次改變的是 attributes,屬性是 style:


第二次改變的是 childList,添加了一個節(jié)點:

第三次也是改變的 childList,刪除了一個節(jié)點:

都監(jiān)聽到了!
這個可以用來做什么呢?比如文章水印被人通過 devtools 去掉了,那么就可以通過 MutationObserver 監(jiān)聽這個變化,然后重新加上,讓水印去不掉。
當然,還有很多別的用途,這里只是介紹功能。
除了監(jiān)聽元素的可見性、屬性和子節(jié)點的變化,還可以監(jiān)聽大小變化:
ResizeObserver
窗口我們可以用 addEventListener 監(jiān)聽 resize 事件,那元素呢?
元素可以用 ResizeObserver 監(jiān)聽大小的改變,當 width、height 被修改時會觸發(fā)回調(diào)。
我們準備這樣一個元素:
<div id="box"></div>
添加樣式:
#box {
width: 100px;
height: 100px;
background: blue;
}
在 2s 的時候修改它的高度:
const box = document.querySelector('#box');
setTimeout(() => {
box.style.width = '200px';
}, 3000);
然后我們用 ResizeObserver 監(jiān)聽它的變化:
const resizeObserver = new ResizeObserver(entries => {
console.log('當前大小', entries)
});
resizeObserver.observe(box);
在瀏覽器跑一下:

大小變化被監(jiān)聽到了,看下打印的信息:

可以拿到元素和它的位置、尺寸。
這樣我們就實現(xiàn)了對元素的 resize 的監(jiān)聽。
除了元素的大小、可見性、屬性子節(jié)點等變化的監(jiān)聽外,還支持對 performance 錄制行為的監(jiān)聽:
PerformanceObserver
瀏覽器提供了 performance 的 api 用于記錄一些時間點、某個時間段、資源加載的耗時等。
我們希望記錄了 performance 那就馬上上報,可是怎么知道啥時候會記錄 performance 數(shù)據(jù)呢?
用 PeformanceObserver。
PerformanceObserver 用于監(jiān)聽記錄 performance 數(shù)據(jù)的行為,一旦記錄了就會觸發(fā)回調(diào),這樣我們就可以在回調(diào)里把這些數(shù)據(jù)上報。
比如 performance 可以用 mark 方法記錄某個時間點:
performance.mark('registered-observer');用 measure 方法記錄某個時間段:
performance.measure('button clicked', 'from', 'to');
后兩個個參數(shù)是時間點,不傳代表從開始到現(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);// 上報
})
});
performanceObserver.observe({entryTypes: ['resource', 'mark', 'measure']});
performance.mark('registered-observer');
function measureClick() {
performance.measure('button clicked');
}
</script>
</body>
</html>
創(chuàng)建 PerformanceObserver 對象,監(jiān)聽 mark(時間點)、measure(時間段)、resource(資源加載耗時) 這三種記錄時間的行為。
然后我們用 mark 記錄了某個時間點,點擊 button 的時候用 measure 記錄了某個時間段的數(shù)據(jù),還加載了一個圖片。
當這些記錄行為發(fā)生的時候,希望能觸發(fā)回調(diào),在里面可以上報。
我們在瀏覽器跑一下試試:

可以看到 mark 的時間點記錄、資源加載的耗時、點擊按鈕的 measure 時間段記錄都監(jiān)聽到了。
分別打印了這三種記錄行為的數(shù)據(jù):
mark:

圖片加載:

measure:

用了這些數(shù)據(jù),就可以上報上去做性能分析了。
除了元素、performance 外,瀏覽器還有一個 reporting 的監(jiān)聽:
ReportingObserver
當瀏覽器運行到過時(deprecation)的 api 的時候,會在控制臺打印一個過時的報告:

瀏覽器還會在一些情況下對網(wǎng)頁行為做一些干預(yù)(intervention),比如會把占用 cpu 太多的廣告的 iframe 刪掉:

會在網(wǎng)絡(luò)比較慢的時候把圖片替換為占位圖片,點擊才會加載:

這些干預(yù)都是瀏覽器做的,會在控制臺打印一個報告:

這些干預(yù)或者過時的 api 并不是報錯,所以不能用錯誤監(jiān)聽的方式來拿到,但這些情況對網(wǎng)頁 app 來說可能也是很重要的:
比如我這個網(wǎng)頁就是為了展示廣告的,但瀏覽器一干預(yù)給我把廣告刪掉了,我卻不知道。如果我知道的話或許可以優(yōu)化下 iframe。
比如我這個網(wǎng)頁的圖片很重要,結(jié)果瀏覽器一干預(yù)給我換成占位圖了,我卻不知道。如果我知道的話可能會優(yōu)化下圖片大小。
所以自然也要監(jiān)聽,所以瀏覽器提供了 ReportingObserver 的 api 用來監(jiān)聽這些報告的打印,我們可以拿到這些報告然后上傳。
const reportingObserver = new ReportingObserver((reports, observer) => {
for (const report of reports) {
console.log(report.body);//上報
}
}, {types: ['intervention', 'deprecation']});
reportingObserver.observe();
ReportingObserver 可以監(jiān)聽過時的 api、瀏覽器干預(yù)等報告等的打印,在回調(diào)里上報,這些是錯誤監(jiān)聽無法監(jiān)聽到但對了解網(wǎng)頁運行情況很有用的數(shù)據(jù)。
文中的代碼上傳到了 github:https://github.com/QuarkGluonPlasma/browser-api-exercize
總結(jié)
監(jiān)聽用戶的交互行為,我們會用 addEventListener 來監(jiān)聽 click、mousedown、keydown、input 等事件,但對于元素的變化、performance 的記錄、瀏覽器干預(yù)行為這些不是用戶交互的事件就要用 XxxObserver 的 api 了。
瀏覽器提供了這 5 種 Observer:
- IntersectionObserver:監(jiān)聽元素可見性變化,常用來做元素顯示的數(shù)據(jù)采集、圖片的懶加載
- MutationObserver:監(jiān)聽元素屬性和子節(jié)點變化,比如可以用來做去不掉的水印
- ResizeObserver:監(jiān)聽元素大小變化
- 還有兩個與元素無關(guān)的:
- PerformanceObserver:監(jiān)聽 performance 記錄的行為,來上報數(shù)據(jù)
- ReportingObserver:監(jiān)聽過時的 api、瀏覽器的一些干預(yù)行為的報告,可以讓我們更全面的了解網(wǎng)頁 app 的運行情況
這些 api 相比 addEventListener 添加的交互事件來說用的比較少,但是在特定場景下都是很有用的。
瀏覽器的 5 種 Observer,你用過幾種呢?在什么情況下用到過呢?




























