前段時間在研究前端異常監控/埋點平臺的實現。css
在思考方案時,想到了瀏覽器自帶的觀察者以及頁面生命週期API 。html
因而在翻查資料時意外發現,原來現代瀏覽器支持多達四種不一樣類型的觀察者:前端
Intersection Observer
,交叉觀察者。Mutation Observer
,變更觀察者。Resize Observer
,視圖觀察者。Performance Observer
,性能觀察者
IntersectionObserver | MutationObserver | ResizeObserver | PerformanceObserver | |
---|---|---|---|---|
用途 | 觀察一個元素是否在視窗可見 | 觀察DOM中的變化 | 觀察視口大小的變化 | 監測性能度量事件 |
方法 | observe() disconnect() takeRecords() |
observe() disconnect() takeRecords() unobserve() |
observe() disconnect() unobserve() |
observe() disconnect() takeRecords() |
取代 | Dom Mutation events | getBoundingClientRect() 返回元素的大小及其相對於可視窗口的位置 Scroll 和 Resize 事件 |
Resize 事件 | Performance 接口 |
用途 | 1. 無限滾動 2. 圖片懶加載 3. 興趣埋點 4. 控制動畫/視頻執行(性能優化) |
1. 更高性能的數據綁定及響應 2. 實現視覺差滾動 3. 圖片預加載 4. 實現富文本編輯器 |
1. 更智能的響應式佈局(取代@media) 2. 響應式組件 |
1. 更細顆粒的性能監控 2. 分析性能對業務的影響(交互快/慢是否會影響銷量) |
IntersectionObserver
:交叉觀察者
IntersectionObserver
接口,提供了一種異步觀察目標元素與其祖先元素或頂級文檔視窗(viewport
)交叉狀態的方法,祖先元素與視窗(viewport)
被稱爲根(root
)vue
想要計算Web頁面的元素的位置,很是依賴於DOM
狀態的顯式查詢。但這些查詢是同步的,會致使昂貴的樣式計算開銷(重繪和迴流),且不停輪詢會致使大量的性能浪費。git
scroll
等事件或經過插件的形式,計算真實元素可見性。而它們都有幾項共同特色:github
web
平臺支持匱乏,各有各家的處理。須要開發人員消耗大量精力兼容。IntersectionObserver
的優點Intersection Observer API
經過爲開發人員提供一種新方法來異步查詢元素相對於其餘元素或全局視口的位置,從而解決了上述問題:web
DOM
和樣式查詢,連續輪詢以及使用自定義插件的需求。CPU
,GPU
和資源成本。IntersectionObserver
基本使用使用IntersectionObserver API
主要須要三個步驟:api
const options = {
root: document.querySelector('.scrollContainer'),
rootMargin: '0px',
threshold: [0.3, 0.5, 0.8, 1] }
const observer = new IntersectionObserver(handler, options)
複製代碼
這幾個參數用大白話解釋就是:數組
root
:指定一個根元素rootMargin
:使用相似於設置CSS邊距的語法來指定根邊距(根元素的觀察影響範圍)
threshold
:閾值,能夠爲數組。[0.3]
意味着,當目標元素在根元素指定的元素內可見30%時,調用處理函數。當目標元素與根元素經過閾值相交時,就會觸發回調函數。瀏覽器
function handler (entries, observer) {
entries.forEach(entry => {
// 每一個成員都是一個IntersectionObserverEntry對象。
// 舉例來講,若是同時有兩個被觀察的對象的可見性發生變化,entries數組就會有兩個成員。
// entry.boundingClientRect
// entry.intersectionRatio
// entry.intersectionRect
// entry.isIntersecting
// entry.rootBounds
// entry.target
// entry.time
});
}
複製代碼
任何目標元素均可以經過調用.observer(target)
方法來觀察。
const target = document.querySelector(「.targetBox」);
observer.observe(target);
複製代碼
此外,還有兩個方法:
中止對某目標的監聽
observer.unobserve(target)
複製代碼
終止對全部目標的監聽
observer.disconnect()
複製代碼
HTML
:
<img src="placeholder.png" data-src="img-1.jpg">
<img src="placeholder.png" data-src="img-2.jpg">
<img src="placeholder.png" data-src="img-3.jpg">
<!-- more images -->
複製代碼
腳本:
let observer = new IntersectionObserver(
(entries, observer) => {
entries.forEach(entry => {
/* 替換屬性 */
entry.target.src = entry.target.dataset.src;
observer.unobserve(entry.target);
});
},
{rootMargin: "0px 0px -200px 0px"});
document.querySelectorAll('img').forEach(img => { observer.observe(img) });
複製代碼
上述例子表示 僅在到達視口距離底部200px視加載圖片。
關於興趣埋點,一個比較通用的方案是:
const boxList = [...document.querySelectorAll('.box')]
var io = new IntersectionObserver((entries) =>{
entries.forEach(item => {
// intersectionRatio === 1說明該元素徹底暴露出來,符合業務需求
if (item.intersectionRatio === 1) {
// 。。。 埋點曝光代碼
io.unobserve(item.target)
}
})
}, {
root: null,
threshold: 1, // 閥值設爲1,當只有比例達到1時才觸發回調函數
})
// observe遍歷監聽全部box節點
boxList.forEach(box => io.observe(box))
複製代碼
至於怎樣評斷用戶是否感興趣,記錄方式就見仁見智了:
PC
端記錄鼠標點擊次數/懸停時間,移動端記錄touch
事件這裏就不展開寫了(我懶)。
這裏提供控制視頻的版本
HTML
:
<video src="OSRO-animation.mp4" controls=""></video>
複製代碼
js
:
let video = document.querySelector('video');
let isPaused = false; /* Flag for auto-paused video */
let observer = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if(entry.intersectionRatio!=1 && !video.paused){
video.pause(); isPaused = true;
}
else if(isPaused) {video.play(); isPaused=false}
});
}, {threshold: 1});
observer.observe(video);
複製代碼
效果:
Mutation Observer
:變更觀察者接口提供了監視對
DOM
樹所作更改的能力。它被設計爲舊的MutationEvents
功能的替代品,該功能是DOM3 Events
規範的一部分。
MutationEvents
的功能不盡人意:
MDN
中也寫到了,是被DOM Event
認可在API上有缺陷,反對使用。DOM
添加 mutation
監聽器極度下降進一步修改DOM
文檔的性能(慢1.5 - 7倍),此外, 移除監聽器不會逆轉的損害。MutationEvents
的原理:經過綁定事件監聽DOM
乍一看到感受很正常,那列一下相關監聽的事件:
DOMAttributeNameChanged
DOMCharacterDataModified
DOMElementNameChanged
DOMNodeInserted
DOMNodeInsertedIntoDocument
DOMNodeRemoved
DOMNodeRemovedFromDocument
DOMSubtreeModified
複製代碼
甭記,這麼多事件,各內核各版本瀏覽器想兼容怕是要天荒地老。
MutationObserver
的優點而Mutation Observer
的優點在於:
MutationEvents
事件是同步觸發,也就是說,DOM
的變更馬上會觸發相應的事件;Mutation Observer
則是異步觸發,DOM
的變更並不會立刻觸發,而是要等到當前全部 DOM
操做都結束才觸發。DOM
下子元素的變動記錄簡單講:異步萬歲
!
MutationObserver
基本使用使用MutationObserver API
主要須要三個步驟:
let observer = new MutationObserver(callback);
複製代碼
上面代碼中的回調函數,會在每次 DOM 變更後調用。該回調函數接受兩個參數,第一個是變更數組,第二個是觀察器實例,下面是一個例子:
function callback (mutations, observer) {
mutations.forEach(function(mutation) {
console.log(mutation);
});
});
複製代碼
其中每一個mutation
都對應一個MutationRecord
對象,記錄着DOM
每次發生變化的變更記錄
MutationRecord
對象包含了DOM的相關信息,有以下屬性:
屬性 | 意義 |
---|---|
type |
觀察的變更類型(attribute 、characterData 或者childList ) |
target |
發生變更的DOM 節點 |
addedNodes |
新增的DOM 節點 |
removedNodes |
刪除的DOM 節點 |
previousSibling |
前一個同級節點,若是沒有則返回null |
nextSibling |
下一個同級節點,若是沒有則返回null |
attributeName |
發生變更的屬性。若是設置了attributeFilter ,則只返回預先指定的屬性 |
oldValue |
變更前的值。這個屬性只對attribute 和characterData 變更有效,若是發生childList 變更,則返回null |
MutationObserver.observe(dom, options)
複製代碼
啓動監聽,接收兩個參數。
DOM
節點。options
。mutationObserver.observe(content, {
attributes: true, // Boolean - 觀察目標屬性的改變
characterData: true, // Boolean - 觀察目標數據的改變(改變前的數據/值)
childList: true, // Boolean - 觀察目標子節點的變化,好比添加或者刪除目標子節點,不包括修改子節點以及子節點後代的變化
subtree: true, // Boolean - 目標以及目標的後代改變都會觀察
attributeOldValue: true, // Boolean - 表示須要記錄改變前的目標屬性值
characterDataOldValue: true, // Boolean - 設置了characterDataOldValue能夠省略characterData設置
// attributeFilter: ['src', 'class'] // Array - 觀察指定屬性
});
複製代碼
優先級 :
attributeFilter/attributeOldValue
> attributes
characterDataOldValue
> characterData
attributes/characterData/childList
(或更高級特定項)至少有一項爲true;true
此外,還有兩個方法:
中止觀察。調用後再也不觸發觀察器,解除訂閱
MutationObserver.disconnect()
複製代碼
清除變更記錄。即再也不處理未處理的變更。該方法返回變更記錄的數組,注意,該方法當即生效。
MutationObserver.takeRecords()
複製代碼
MutationObserver
監聽文本變化基本使用是:
const target = document.getElementById('target-id')
const observer = new MutationObserver(records => {
// 輸入變動記錄
})
// 開始觀察
observer.observe(target, {
characterData: true
})
複製代碼
這裏能夠有幾種處理。
#
」號時,啓動搜索框預檢文本或高亮話題。有個Vue
的小型插件就是這麼實現的:
這個實現也是秀得飛起:
遊戲的邏輯很簡單,當中間的色塊顏色改變時,在時間限制內於底下的選項選擇跟它顏色同樣的選項就得分。難的點在於越後面的關卡選項越多,並且選項顏色也越相近,例如:
其實原理很是簡單,就是觀察色塊的backgroundColor
(屬性變化attributes
),而後觸發點擊事件e.click()
。
var targetNode = document.querySelector('#kolor-kolor');
var config = { attributes: true };
var callback = function(mutationsList, observer) {
if (mutationsList[0].type == 'attributes') {
console.log('attribute change!');
let ans = document.querySelector('#kolor-kolor').style.backgroundColor;
document.querySelectorAll('#kolor-options a').forEach( (e) => {
if (e.style.backgroundColor == ans) {
e.text = 'Ans!';
e.click()
}
})
}
};
var observer = new MutationObserver(callback);
observer.observe(targetNode, config);
複製代碼
ResizeObserver
,視圖觀察者ResizeObserver API
是一個新的JavaScript API
,與IntersectionObserver API
很是類似,它們都容許咱們去監聽某個元素的變化。
開發過程中常常遇到的一個問題就是如何監聽一個 div
的尺寸變化。
但衆所周知,爲了監聽 div
的尺寸變化,都將偵聽器附加到 window
中的 resize
事件。
但這很容易致使性能問題,由於大量的觸發事件。
換句話說,使用 window.resize
一般是浪費的,由於它告訴咱們每一個視窗大小的變化,而不只僅是當一個元素的大小發生變化。
並且resize
事件會在一秒內觸發將近60次,很容易在改變窗口大小時致使性能問題
好比說,你要調整一個元素的大小,那就須要在 resize
的回調函數 callback()
中調用 getBoundingClientRect
或 getComputerStyle
。不過你要是不當心處理全部的讀和寫操做,就會致使佈局混亂。好比下面這個小示例:
ResizeObserver
的優點ResizeObserver API
的核心優點有兩點:
DOM
元素觀察,而不是window
ResizeObserver
基本使用使用ResizeObserver API
一樣也是三個步驟:
let observer = new ResizeObserver(callback);
複製代碼
const callback = entries => {
entries.forEach(entry => {
})
}
複製代碼
每個entry
都是一個對象,包含兩個屬性contentRect
和target
contentRect
都是一些位置信息:
屬性 | 做用 |
---|---|
bottom |
top + height 的值 |
height |
元素自己的高度,不包含padding ,border 值 |
left |
padding-left 的值 |
right |
left + width 的值 |
top |
padidng-top 的值 |
width |
元素自己的寬度,不包含padding ,border 值 |
x |
大小與top 相同 |
y |
大小與left 相同 |
observer.observe(document.body)
複製代碼
unobserve
方法:取消單節點觀察
observer.unobserve(document.body)
複製代碼
disconnect
方法:取消全部節點觀察
observer.disconnect(document.body)
複製代碼
html
:
<div class="box">
<h3 class="info"></h3>
</div>
<div class="box small">
<h3 class="info"></h3>
</div>
複製代碼
添加點樣式:
body {
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
padding: 2vw;
box-sizing: border-box;
}
.box {
text-align: center;
height: 20vh;
border-radius: 8px;
box-shadow: 0 0 4px rgba(0,0,0,.25);
display: flex;
justify-content: center;
align-items: center;
padding: 1vw
}
.box h3 {
color: #fff;
margin: 0;
font-size: 5vmin;
text-shadow: 0 0 10px rgba(0,0,0,0.4);
}
.box.small {
max-width: 550px;
margin: 1rem auto;
}
複製代碼
JavaScript
代碼:
const boxes = document.querySelectorAll('.box');
let callbackFired = 0;
const myObserver = new ResizeObserver(entries => {
for (let entry of entries) {
callbackFired++
const infoEl = entry.target.querySelector('.info');
const width = Math.floor(entry.contentRect.width);
const height = Math.floor(entry.contentRect.height);
const angle = Math.floor(width / 360 * 100);
const gradient = `linear-gradient(${ angle }deg, rgba(0,143,104,1) 50%, rgba(250,224,66,1) 50%)`;
entry.target.style.background = gradient;
infoEl.innerText = `
I'm ${ width }px and ${ height }px tall Callback fired: ${callbackFired} `; } }); boxes.forEach(box => { myObserver.observe(box); }); 複製代碼
當你拖動瀏覽器窗口,改變其大小時,看到的效果以下:
Vue
組件簡單的@media
就能夠實現:
@media only screen and (max-width: 576px) {
.post__item {
flex-direction: column;
}
.post__image {
flex: 0 auto;
height: auto;
}
}
複製代碼
@media
查詢的最大問題是:
如下是指令版實現:
效果:
這是vue-responsive-components
庫的具體實現代碼,還有組件形式的實現,感興趣的能夠去看看。
PerformanceObserver
:性能觀察者這是一個瀏覽器和Node.js
裏都存在的API,採用相同W3C
的Performance Timeline
規範
window.performance
和 window.PerformanceObserver
。Node.js
程序中須要perf_hooks
取得性能對象,以下:const { PerformanceObserver, performance } = require('perf_hooks');
複製代碼
首先來看Performance
接口:
能夠獲取到當前頁面中與性能相關的信息。它是 High Resolution Time API
的一部分,同時也融合了 Performance Timeline API
、Navigation Timing AP
、 User Timing API
和 Resource Timing API
。
Performance API
是你們熟悉的一個接口,他記錄着幾種性能指數的龐大對象集合。
performance.getEntries
或者performance.getEntriesByName
來得到。performance.now
來計算。爲了解決上述的問題,在Performance Timeline Level 2
中,除了擴展了Performance
的基本定義之外,還增長了PerformanceObserver
接口。
PerformanceObserver
的優點PerformanceObserver
是瀏覽器內部對Performance
實現的觀察者模式,也是現代瀏覽器支持的幾個 Observer
之一。
它解決了如下3點問題:
timeline
獲取記錄。W3C
官網文檔鼓勵開發人員儘量使用PerformanceObserver
,而不是經過Performance
獲取性能參數及指標。
PerformanceObserver
的使用使用PerformanceObserver API
主要須要三個步驟:
let observer = new PerformanceObserver(callback);
複製代碼
const callback = (list, observer) => {
const entries = list.getEntries();
entries.forEach((entry) => {
console.log(「Name: 「 + entry.name + 「, Type: 「 + entry.entryType + 「, Start: 「 + entry.startTime + 「, Duration: 「 + entry.duration + 「\n」); });
}
複製代碼
其中每個list
都是一個完整的PerformanceObserverEntryList
對象:
getEntries
、
getEntriesByType
、
getEntriesByName
:
方法 | 做用 |
---|---|
getEntries() | 返回一個列表,該列表包含一些用於承載各類性能數據的對象,不作任何過濾 |
getEntriesByType() | 返回一個列表,該列表包含一些用於承載各類性能數據的對象,按類型過濾 |
getEntriesByName() | 返回一個列表,,該列表包含一些用於承載各類性能數據的對象,按名稱過濾 |
observer.observe({entryTypes: ["entryTypes"]});
複製代碼
observer.observe(...)
方法接受能夠觀察到的有效的入口類型。這些輸入類型可能屬於各類性能API,好比User tming
或Navigation Timing API
。有效的entryType
值:
屬性 | 別名 | 類型 | 描述 |
---|---|---|---|
frame, navigation | PerformanceFrameTiming, PerformanceNavigationTiming | URL | 文件的地址。 |
resource | PerformanceResourceTiming | URL | 所請求資源的解析URL。 |
mark | PerformanceMark | DOMString | 經過調用建立標記時使用的名稱performance.mark()。 |
measure | PerformanceMeasure | DOMString | 經過調用建立度量時使用的名稱performance.measure()。 |
paint | PerformancePaintTiming | DOMString | 不管是'first-paint'或'first-contentful-paint'。 |
longtask | PerformanceLongTaskTiming | DOMString | 報告長任務的實例 |
function filterTime(a, b) {
return (a > 0 && b > 0 && (a - b) >= 0) ? (a - b) : undefined;
}
let resolvePerformanceTiming = (timing) => {
let o = {
initiatorType: timing.initiatorType,
name: timing.name,
duration: parseInt(timing.duration),
redirect: filterTime(timing.redirectEnd, timing.redirectStart), // 重定向
dns: filterTime(timing.domainLookupEnd, timing.domainLookupStart), // DNS解析
connect: filterTime(timing.connectEnd, timing.connectStart), // TCP建連
network: filterTime(timing.connectEnd, timing.startTime), // 網絡總耗時
send: filterTime(timing.responseStart, timing.requestStart), // 發送開始到接受第一個返回
receive: filterTime(timing.responseEnd, timing.responseStart), // 接收總時間
request: filterTime(timing.responseEnd, timing.requestStart), // 總時間
ttfb: filterTime(timing.responseStart, timing.requestStart), // 首字節時間
};
return o;
};
let resolveEntries = (entries) => entries.map(item => resolvePerformanceTiming(item));
let resources = {
init: (cb) => {
let performance = window.performance || window.mozPerformance || window.msPerformance || window.webkitPerformance;
if (!performance || !performance.getEntries) {
return void 0;
}
if (window.PerformanceObserver) {
let observer = new window.PerformanceObserver((list) => {
try {
let entries = list.getEntries();
cb(resolveEntries(entries));
} catch (e) {
console.error(e);
}
});
observer.observe({
entryTypes: ['resource']
})
} else {
window.addEventListener('load', () => {
let entries = performance.getEntriesByType('resource');
cb(resolveEntries(entries));
});
}
},
};
複製代碼
參考文章有點多:
- 資源監控
- Media Queries Based on Element Width with MutationObserver
- 以用戶爲中心的性能指標
- A Few Functional Uses for Intersection Observer to Know When an Element is in View
- Getting To Know The MutationObserver API
- Different Types Of Observers Supported By Modern Browsers
- THE RESIZE OBSERVER EXPLAINED
- A Look at the Resize Observer JavaScript API 這四個觀察者,都很是適合集成到監控系統。
且都有對應的Polyfills
版實現。
網上的總結和文檔都深淺不一,若是哪裏有錯誤,歡迎指正。
若是你以爲這篇內容對你挺有啓發,我想邀請你幫我三個小忙:
也能夠來個人GitHub
博客裏拿全部文章的源文件:
前端勸退指南:github.com/roger-hiro/…