現代瀏覽器觀察者 Observer API 指南(新)

前言

前段時間在研究前端異常監控/埋點平臺的實現。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. 分析性能對業務的影響(交互快/慢是否會影響銷量)

1. IntersectionObserver:交叉觀察者

IntersectionObserver接口,提供了一種異步觀察目標元素與其祖先元素或頂級文檔視窗(viewport)交叉狀態的方法,祖先元素與視窗(viewport)被稱爲根(root)vue

1. 出現的意義

想要計算Web頁面的元素的位置,很是依賴於DOM狀態的顯式查詢。但這些查詢是同步的,會致使昂貴的樣式計算開銷(重繪和迴流),且不停輪詢會致使大量的性能浪費。git

因而便發展瞭如下的幾種方案:

  • 構建DOM和數據的自定義預加載和延遲加載。
  • 實現了數據綁定的高性能滾動列表,該列表加載和呈現數據集的子集。
  • 經過scroll等事件或經過插件的形式,計算真實元素可見性。

而它們都有幾項共同特色:github

  1. 基本實現形式都是查詢各個元素相對與某些元素(全局視口)的「被動查詢」。
  2. 信息能夠異步傳遞(例如從另外一個線程傳遞),且沒有統一捕獲錯誤的處理。
  3. web平臺支持匱乏,各有各家的處理。須要開發人員消耗大量精力兼容。

2. IntersectionObserver的優點

Intersection Observer API經過爲開發人員提供一種新方法來異步查詢元素相對於其餘元素或全局視口的位置,從而解決了上述問題:web

  • 異步處理消除了昂貴的DOM和樣式查詢,連續輪詢以及使用自定義插件的需求。
  • 經過消除對這些方法的需求,可使應用程序顯着下降CPUGPU和資源成本。

3. IntersectionObserver基本使用

使用IntersectionObserver API主要須要三個步驟:api

  1. 建立觀察者
  2. 定義回調事件
  3. 定義要觀察的目標對象

1.建立觀察者

const options = {
    root: document.querySelector('.scrollContainer'),
    rootMargin: '0px',
    threshold: [0.3, 0.5, 0.8, 1] }
    
const observer = new IntersectionObserver(handler, options)
複製代碼

這幾個參數用大白話解釋就是:數組

  1. root:指定一個根元素
  2. rootMargin:使用相似於設置CSS邊距的語法來指定根邊距(根元素的觀察影響範圍)
  3. threshold:閾值,能夠爲數組。[0.3]意味着,當目標元素在根元素指定的元素內可見30%時,調用處理函數。

2. 定義回調事件

當目標元素與根元素經過閾值相交時,就會觸發回調函數。瀏覽器

function handler (entries, observer) { 
    entries.forEach(entry => { 
    // 每一個成員都是一個IntersectionObserverEntry對象。
    // 舉例來講,若是同時有兩個被觀察的對象的可見性發生變化,entries數組就會有兩個成員。
    // entry.boundingClientRect 
    // entry.intersectionRatio 
    // entry.intersectionRect 
    // entry.isIntersecting 
    // entry.rootBounds 
    // entry.target 
    // entry.time 
    }); 
}
複製代碼
  • time 時間戳
  • rootBounds 根元素的位置
  • boundingClientRect 目標元素的位置信息
  • intersectionRect 交叉部分的位置信息
  • intersectionRatio 目標元素的可見比例,看下圖示
  • target。

3. 定義要觀察的目標對象

任何目標元素均可以經過調用.observer(target)方法來觀察。

const target = document.querySelector(「.targetBox」); 
observer.observe(target);
複製代碼

此外,還有兩個方法:

中止對某目標的監聽

observer.unobserve(target)
複製代碼

終止對全部目標的監聽

observer.disconnect()
複製代碼

4. 例子1:圖片懶加載

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視加載圖片。

5. 例子2:興趣埋點

關於興趣埋點,一個比較通用的方案是:

來自:《超好用的API之IntersectionObserver》

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))
複製代碼

至於怎樣評斷用戶是否感興趣,記錄方式就見仁見智了:

  • 位於屏幕中間,並停留時長大於2秒,計數一次。
  • 區域懸停,觸發定時器記錄時間。
  • PC端記錄鼠標點擊次數/懸停時間,移動端記錄touch事件

這裏就不展開寫了(我懶)。

6. 控制動畫/視頻 執行

這裏提供控制視頻的版本

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);
複製代碼

效果:

2. Mutation Observer:變更觀察者

接口提供了監視對DOM樹所作更改的能力。它被設計爲舊的MutationEvents功能的替代品,該功能是DOM3 Events規範的一部分。

1. 出現的意義

歸根究底,是 MutationEvents的功能不盡人意:

  1. MDN中也寫到了,是被DOM Event認可在API上有缺陷,反對使用。
  2. 核心缺陷是:性能問題和跨瀏覽器支持。
  3. DOM添加 mutation 監聽器極度下降進一步修改DOM文檔的性能(慢1.5 - 7倍),此外, 移除監聽器不會逆轉的損害。

來自:《監聽DOM加載完成及改變——MutationObserver應用》

MutationEvents的原理:經過綁定事件監聽DOM

乍一看到感受很正常,那列一下相關監聽的事件:

DOMAttributeNameChanged
DOMCharacterDataModified
DOMElementNameChanged
DOMNodeInserted
DOMNodeInsertedIntoDocument
DOMNodeRemoved
DOMNodeRemovedFromDocument
DOMSubtreeModified
複製代碼

甭記,這麼多事件,各內核各版本瀏覽器想兼容怕是要天荒地老。

2. MutationObserver的優點

Mutation Observer的優點在於:

  • MutationEvents事件是同步觸發,也就是說,DOM 的變更馬上會觸發相應的事件;
  • Mutation Observer 則是異步觸發,DOM 的變更並不會立刻觸發,而是要等到當前全部 DOM 操做都結束才觸發。
  • 能夠經過配置項,監聽目標DOM下子元素的變動記錄

簡單講:異步萬歲!

3. MutationObserver基本使用

使用MutationObserver API主要須要三個步驟:

  1. 建立觀察者
  2. 定義回調函數
  3. 定義要觀察的目標對象

1. 建立觀察者

let observer = new MutationObserver(callback);

複製代碼

2. 定義回調函數

上面代碼中的回調函數,會在每次 DOM 變更後調用。該回調函數接受兩個參數,第一個是變更數組,第二個是觀察器實例,下面是一個例子:

function callback (mutations, observer) {
  mutations.forEach(function(mutation) {
    console.log(mutation);
  });
});
複製代碼

其中每一個mutation都對應一個MutationRecord對象,記錄着DOM每次發生變化的變更記錄

MutationRecord對象包含了DOM的相關信息,有以下屬性:

屬性 意義
type 觀察的變更類型(attributecharacterData或者childList
target 發生變更的DOM節點
addedNodes 新增的DOM節點
removedNodes 刪除的DOM節點
previousSibling 前一個同級節點,若是沒有則返回null
nextSibling 下一個同級節點,若是沒有則返回null
attributeName 發生變更的屬性。若是設置了attributeFilter,則只返回預先指定的屬性
oldValue 變更前的值。這個屬性只對attributecharacterData變更有效,若是發生childList變更,則返回null

3. 定義要觀察的目標對象

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 - 觀察指定屬性
});
複製代碼

優先級 :

  1. attributeFilter/attributeOldValue > attributes
  2. characterDataOldValue > characterData
  3. attributes/characterData/childList(或更高級特定項)至少有一項爲true;
  4. 特定項存在, 對應選項能夠忽略或必須爲true

此外,還有兩個方法:

中止觀察。調用後再也不觸發觀察器,解除訂閱

MutationObserver.disconnect()
複製代碼

清除變更記錄。即再也不處理未處理的變更。該方法返回變更記錄的數組,注意,該方法當即生效。

MutationObserver.takeRecords()
複製代碼

4. 例子1:MutationObserver監聽文本變化

基本使用是:

const target = document.getElementById('target-id')

const observer = new MutationObserver(records => {
  // 輸入變動記錄
})

// 開始觀察
observer.observe(target, {
  characterData: true
})
複製代碼

這裏能夠有幾種處理。

  • 聊天的氣泡框彩蛋,檢測文本中的指定字符串/表情包,觸發相似微信聊天的表情落下動畫。
  • 輸入框的熱點話題搜索,當輸入「#」號時,啓動搜索框預檢文本或高亮話題。

有個Vue的小型插件就是這麼實現的:

來自:《vue-hashtag-textarea》

5. 例子2: 色塊小遊戲腳本

這個實現也是秀得飛起:

Hacking the color picker game — MutationObserver

遊戲的邏輯很簡單,當中間的色塊顏色改變時,在時間限制內於底下的選項選擇跟它顏色同樣的選項就得分。難的點在於越後面的關卡選項越多,並且選項顏色也越相近,例如:

其實原理很是簡單,就是觀察色塊的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);
複製代碼

3. ResizeObserver,視圖觀察者

ResizeObserver API是一個新的JavaScript API,與IntersectionObserver API很是類似,它們都容許咱們去監聽某個元素的變化。

1. 出現的意義

  • 開發過程中常常遇到的一個問題就是如何監聽一個 div 的尺寸變化。

  • 但衆所周知,爲了監聽 div 的尺寸變化,都將偵聽器附加到 window 中的 resize 事件。

  • 但這很容易致使性能問題,由於大量的觸發事件。

  • 換句話說,使用 window.resize 一般是浪費的,由於它告訴咱們每一個視窗大小的變化,而不只僅是當一個元素的大小發生變化。

  • 並且resize事件會在一秒內觸發將近60次,很容易在改變窗口大小時致使性能問題

好比說,你要調整一個元素的大小,那就須要在 resize 的回調函數 callback() 中調用 getBoundingClientRectgetComputerStyle。不過你要是不當心處理全部的讀和寫操做,就會致使佈局混亂。好比下面這個小示例:

2. ResizeObserver的優點

ResizeObserver API 的核心優點有兩點:

  • 細顆粒度的DOM元素觀察,而不是window
  • 沒有額外的性能開銷,只會在繪製前或佈局後觸發調用

3. ResizeObserver基本使用

使用ResizeObserver API一樣也是三個步驟:

  1. 建立觀察者
  2. 定義回調函數
  3. 定義要觀察的目標對象

1. 建立觀察者

let observer = new ResizeObserver(callback);

複製代碼

2. 定義回調函數

const callback = entries => {
    entries.forEach(entry => {
        
    })
}
複製代碼

每個entry都是一個對象,包含兩個屬性contentRecttarget

contentRect都是一些位置信息:

屬性 做用
bottom top + height的值
height 元素自己的高度,不包含paddingborder
left padding-left的值
right left + width的值
top padidng-top的值
width 元素自己的寬度,不包含paddingborder
x 大小與top相同
y 大小與left相同

3. 定義要觀察的目標對象

observer.observe(document.body)
複製代碼

unobserve方法:取消單節點觀察

observer.unobserve(document.body)
複製代碼

disconnect方法:取消全部節點觀察

observer.disconnect(document.body)
複製代碼

4. 例子1:縮放漸變背景

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); }); 複製代碼

當你拖動瀏覽器窗口,改變其大小時,看到的效果以下:

5. 例子2:響應式Vue組件

  • 假設你要建立一個postItem組件,在大屏上是這樣的顯示效果

  • 在手機上須要這樣的效果:

簡單的@media就能夠實現:

@media only screen and (max-width: 576px) {
  .post__item {
    flex-direction: column;
  }
  
  .post__image {
    flex: 0 auto;
    height: auto;
  }
}
複製代碼
  • 但這就很容易出現 當你在超過預期的屏幕(過大)查看頁面時,會出現如下的佈局:

@media查詢的最大問題是:

  • 組件響應度取決於屏幕尺寸,而不是響應自身的尺寸。

如下是指令版實現:

使用:

效果:

這是vue-responsive-components庫的具體實現代碼,還有組件形式的實現,感興趣的能夠去看看。

4. PerformanceObserver:性能觀察者

這是一個瀏覽器和Node.js 裏都存在的API,採用相同W3CPerformance Timeline規範

  • 在瀏覽器中,咱們可使用 window 對象取得window.performancewindow.PerformanceObserver
  • 而在 Node.js 程序中須要perf_hooks 取得性能對象,以下:
    const { PerformanceObserver, performance } = require('perf_hooks');
    複製代碼

1. 出現的意義

首先來看Performance 接口:

  • 能夠獲取到當前頁面中與性能相關的信息。它是 High Resolution Time API 的一部分,同時也融合了 Performance Timeline APINavigation Timing APUser Timing APIResource Timing API

  • Performance API 是你們熟悉的一個接口,他記錄着幾種性能指數的龐大對象集合。

  1. 若想得到某項頁面加載性能記錄,就須要調用performance.getEntries或者performance.getEntriesByName來得到。
  2. 而得到執行效率,也只能經過performance.now來計算。

爲了解決上述的問題,在Performance Timeline Level 2中,除了擴展了Performance的基本定義之外,還增長了PerformanceObserver接口。

2. PerformanceObserver的優點

PerformanceObserver是瀏覽器內部對Performance實現的觀察者模式,也是現代瀏覽器支持的幾個 Observer 之一。

來自:《你瞭解 Performance Timeline Level 2 嗎?》

它解決了如下3點問題:

  • 避免不知道性能事件啥時候會發生,須要重複輪訓timeline獲取記錄。
  • 避免產生重複的邏輯去獲取不一樣的性能數據指標
  • 避免其餘資源須要操做瀏覽器性能緩衝區時產生競態關係。

W3C官網文檔鼓勵開發人員儘量使用PerformanceObserver,而不是經過Performance獲取性能參數及指標。

3. PerformanceObserver的使用

使用PerformanceObserver API主要須要三個步驟:

  1. 建立觀察者
  2. 定義回調函數事件
  3. 定義要觀察的目標對象

1. 建立觀察者

let observer = new PerformanceObserver(callback); 
複製代碼

2. 定義回調函數事件

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對象:

包含三個方法 getEntriesgetEntriesByTypegetEntriesByName

方法 做用
getEntries() 返回一個列表,該列表包含一些用於承載各類性能數據的對象,不作任何過濾
getEntriesByType() 返回一個列表,該列表包含一些用於承載各類性能數據的對象,按類型過濾
getEntriesByName() 返回一個列表,,該列表包含一些用於承載各類性能數據的對象,按名稱過濾

3. 定義要觀察的目標對象

observer.observe({entryTypes: ["entryTypes"]});
複製代碼

observer.observe(...)方法接受能夠觀察到的有效的入口類型。這些輸入類型可能屬於各類性能API,好比User tmingNavigation 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 報告長任務的實例

4. 例子1:靜態資源監控

來自:《資源監控》

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));
      });
    }
  },
};
複製代碼

參考文章&總結

參考文章有點多:

且都有對應的Polyfills版實現。

網上的總結和文檔都深淺不一,若是哪裏有錯誤,歡迎指正。

❤️ 看完三件事

若是你以爲這篇內容對你挺有啓發,我想邀請你幫我三個小忙:

  1. 點贊,讓更多的人也能看到這篇內容(收藏不點贊,都是耍流氓 -_-)
  2. 關注公衆號「前端勸退師」,不按期分享原創知識。
  3. 也看看其它文章

也能夠來個人GitHub博客裏拿全部文章的源文件:

前端勸退指南github.com/roger-hiro/…

相關文章
相關標籤/搜索