一個神奇的交叉觀察 API Intersection Observer
前言
前端開發肯定離不開判斷一個元素是否能被用戶看見,然後再基於此進行一些交互。
過去,要檢測一個元素是否可見或者兩個元素是否相交併不容易,很多解決辦法不可靠或性能很差。然而,隨着互聯網的發展,這種需求卻與日俱增,比如,下面這些情況都需要用到相交檢測:
圖片懶加載——當圖片滾動到可見時才進行加載
內容無限滾動——也就是用戶滾動到接近內容底部時直接加載更多,而無需用戶操作翻頁,給用戶一種網頁可以無限滾動的錯覺
檢測廣告的曝光情況——爲了計算廣告收益,需要知道廣告元素的曝光情況
在用戶看見某個區域時執行任務或播放動畫
過去,相交檢測通常要用到事件監聽,並且需要頻繁調用
Element.getBoundingClientRect()
方法以獲取相關元素的邊界信息。事件監聽和調用Element.getBoundingClientRect()
都是在主線程上運行,因此頻繁觸發、調用可能會造成性能問題。這種檢測方法極其怪異且不優雅。
上面這一段話來自 MDN
,中心思想就是現在判斷一個元素是否能被用戶看見的使用場景越來越多,監聽 scroll
事件以及通過 Element.getBoundingClientRect()
獲取節點位置的方式,又麻煩又不好用,那麼怎麼辦呢。於是就有了今天的內容 Intersection Observer API。
Intersection Observer API 是什麼
我們需要觀察的元素被稱爲 目標元素 (target),設備視窗或者其他指定的元素視口的邊界框我們稱它爲 根元素 (root),或者簡稱爲 根 。
Intersection Observer API
翻譯過來叫做 “交叉觀察器”,因爲判斷元素是否可見(通常情況下)的本質就是判斷目標元素和根元素是不是產生了 交叉區域 。
爲什麼是通常情況下,因爲當我們 css
設置了 opacity: 0
,visibility: hidden
或者 用其他的元素覆蓋目標元素
的時候,對於視圖來說是不可見的,但對於交叉觀察器來說是可見的。這裏可能有點抽象,大家只需記住,交叉觀察器只關心 目標元素 和 根元素 是否有 交叉區域, 而不管視覺上能不能看見這個元素。當然如果設置了 display:none
,那麼交叉觀察器就不會生效了,其實也很好理解,因爲元素已經不存在了,那麼也就監測不到了。
一句話總結:Intersection Observer API 提供了一種異步檢測目標元素與祖先元素或 viewport 相交情況變化的方法。 -- MDN
現在不懂沒關係,咱們接着往下看,看完自然就明白了。
Intersection Observer API 怎麼玩
生成觀察器
// 調用構造函數 IntersectionObserver 生成觀察器
const myObserver = new IntersectionObserver(callback, options);
複製代碼
首先調用瀏覽器原生構造函數 IntersectionObserver
,構造函數的返回值是一個 觀察器實例 。
構造函數 IntersectionObserver
接收兩個參數
-
callback: 可見性發生變化時觸發的回調函數
-
options: 配置對象(可選,不傳時會使用默認配置)
構造函數接收的參數 options
爲了方便理解,我們先看第二個參數 options
。一個可以用來配置觀察器實例的對象,那麼這個配置對象都包含哪些屬性呢?
-
root: 設置目標元素的根元素,也就是我們用來判斷元素是否可見的區域,必須是目標元素的父級元素,如果不指定的話,則使用瀏覽器視窗,也就是
document
。 -
rootMargin: 一個在計算交叉值時添加至根的邊界中的一組偏移量,類型爲字符串
(string)
,可以有效的縮小或擴大根的判定範圍從而滿足計算需要。語法大致和 CSS 中margin
屬性等同,默認值“0px 0px 0px 0px”
,如果有指定root
參數,則rootMargin
也可以使用百分比來取值。 -
threshold: 介於
0
和1
之間的數字,指示觸發前應可見的百分比。也可以是一個數字數組,以創建多個觸發點,也被稱之爲 閾值。如果構造器未傳入值, 則默認值爲0
。 -
trackVisibility: 一個布爾值,指示當前觀察器是否將跟蹤目標可見性的更改,默認爲
false
,注意,此處的可見性並非指目標元素和根元素是否相交,而是指視圖上是否可見,這個我們之前就已經分析過了,如果此值設置爲false
或不設置,那麼回調函數參數中IntersectionObserverEntry
的isVisible
屬性將永遠返回false
。 -
delay: 一個數字,也就是回調函數執行的延遲時間(毫秒)。如果
trackVisibility
設置爲true
,則此值必須至少設置爲100
,否則會報錯(但是這裏我也只是親測出來的,並不知道爲什麼會設計成這樣,如果有大佬瞭解請指教一下)。
構造函數接收的參數 callback
當元素可見比例超過指定閾值後,會調用一個回調函數,此回調函數接受兩個參數:存放 IntersectionObserverEntry
對象的數組和觀察器實例 (可選)。
((doc) => {
//回調函數
const callback = (entries, observer) => {
console.log('🚀🚀~ 執行了一次callback');
console.log('🚀🚀~ entries:', entries);
console.log('🚀🚀~ observer:', observer);
};
//配置對象
const options = {};
//創建觀察器
const myObserver = new IntersectionObserver(callback, options);
//獲取目標元素
const target = doc.querySelector(".target")
//開始監聽目標元素
myObserver.observe(target);
})(document)
我們把這兩個參數打印出來看一下,可以看到,第一個參數是一個數組,每一項都是一個目標元素對應的 IntersectionObserverEntry
對象,第二個參數是觀察器實例對象 IntersectionObserver
。
什麼是 IntersectionObserverEntry 對象
展開 IntersectionObserverEntry
看一下都有什麼。
-
boundingClientRect: 一個對象,包含目標元素的
getBoundingClientRect()
方法的返回值。 -
intersectionRatio: 一個對象,包含目標元素與根元素交叉區域
getBoundingClientRect()
的返回值。 -
intersectionRect: 目標元素的可見比例,即
intersectionRect
佔boundingClientRect
的比例,完全可見時爲1
,完全不可見時小於等於0
。 -
isIntersecting: 返回一個布爾值,如果目標元素與根元素相交,則返回
true
,如果isIntersecting
是true
,則target
元素至少已經達到thresholds
屬性值當中規定的其中一個閾值,如果是false
,target
元素不在給定的閾值範圍內可見。 -
isVisible: 這個看字面意思應該是 “是否可見” ,如果要讓這個屬性生效,那麼在使用構造函數生成觀察器實例的時候,傳入的
options
參數必須配置trackVisibility
爲true
,並且delay
設置爲大於100
,否則該屬性將永遠返回false
。 -
rootBounds: 一個對象,包含根元素的
getBoundingClientRect()
方法的返回值。 -
target:: 被觀察的目標元素,是一個
DOM
節點。在觀察者包含多個目標的情況下,這是確定哪個目標元素觸發了此相交更改的簡便方法。 -
time: 該屬性提供從 首次創建觀察者 到 觸發此交集改變 的時間(以毫秒爲單位)。通過這種方式,你可以跟蹤觀察器達到特定閾值所花費的時間。即使稍後將目標再次滾動到視圖中,此屬性也會提供新的時間。這可用於跟蹤目標元素進入和離開根元素的時間,以及兩個閾值觸發的間隔時間。
這裏再看一下 boundingClientRect ,intersectionRatio , rootBounds 三個屬性展開的內容都有什麼。
-
bottom: 元素下邊距離頁面上邊的距離
-
left: 元素左邊距離頁面左邊的距離
-
right: 元素右邊距離頁面左邊的距離
-
top: 元素上邊距離頁面上邊的距離
-
width: 元素的寬
-
height: 元素的高
-
x: 等同於
left
,元素左邊距離頁面左邊的距離 -
y: 等同於
top
,元素上邊距離頁面上邊的距離
用一張圖來展示一下這幾個屬性,特別需要注意的是 right
和 bottom
,跟我們平時寫 css
的 position
那個不一樣 。
那麼第二個參數 IntersectionObserver 觀察器實例對象都有什麼呢
彆着急,接着往下看,實例屬性部分。
觀察器實例屬性
上面留了一個坑,回調函數的第二個參數 IntersectionObserver
觀察器實例對象都有什麼呢?我們把實例對象打印出來看一下
((doc) => {
//回調函數
const callback = () => {};
//配置對象
const options = {};
//創建觀察器
const myObserver = new IntersectionObserver(callback, options);
//獲取目標元素
const target = doc.querySelector(".target")
//開始監聽目標元素
myObserver.observe(target);
console.log('🚀🚀~ myObserver:', myObserver);
})(document)
-
root
-
rootMargin
-
thresholds
-
trackVisibility
-
delay
是不是特別眼熟,沒錯,就是我們創建觀察者實例的時候,傳入的 options
對象,只不過 options
對象是可選的,觀察器實例的屬性就使用我們傳入的 options
對象,如果沒傳就使用默認值,唯一不同的是,options
中 的屬性 threshold
是單數,而我們實例獲取到的 thresholds
是複數。
值得注意的是,這裏的所有屬性都是 只讀 的,也就是說一旦觀察器被創建,則 無法 更改其配置,所以一個給定的觀察者對象只能用來監聽可見區域的特定變化值。
接下來我們就通過代碼結合動圖演示一下這些屬性
((doc) => {
let n = 0
//獲取目標元素
const target = doc.querySelector(".target")
//獲取根元素
const root = doc.querySelector(".out-container")
//回調函數
const callback = (entries, observer) => {
n++
console.log(`🚀🚀~ 執行了 ${n} 次callback`);
console.log('🚀🚀~ entries:', entries);
console.log('🚀🚀~ observer:', observer);
};
//配置對象
const options = {
root: root,
rootMargin: '0px 0px 0px 0px',
threshold: [0.5],
trackVisibility: true,
delay: 100
};
//創建觀察器
const myObserver = new IntersectionObserver(callback, options);
//開始監聽目標元素
myObserver.observe(target);
console.log('🚀🚀~ myObserver:', myObserver);
})(document)
root 這個沒什麼說的,就是設置指定節點爲根元素rootMargin
修改爲 '50px 50px 50px 50px'
,可以看到,我們的目標元素還沒有露出來的時候回調函數就已經執行了,也就是說目標元素距離根元素還有 50px
的 margin
時,觀察器就認爲是發生了交叉。threshold
修改爲 [0.1, 0.3, 0.5, 0.8, 1]
, 可以看到,回調函數觸發了多次,也就是說當交叉區域的百分比,每達到指定的閾值時都會觸發一次回調函數。
注意
Intersection Observer API
無法提供重疊的像素個數或者具體哪個像素重疊,他的更常見的使用方式是——當兩個元素相交比例在N%
左右時,觸發回調,以執行某些邏輯。 -- MDN
trackVisibility 修改 trackVisibility
爲 true
,可以看到, isVisible
屬性值爲 true
。css
屬性 爲 opacity: 0
,可以看到,雖然我們藍色小方塊並沒有出現在視圖中,但是回調函數已經執行了,並且 isVisible
屬性值爲 false
而 isIntersecting
值爲 true
。delay
爲 3000
,可以看到 log
是 3000ms
以後才輸出的。
觀察器實例方法
通過此段代碼來演示觀察器實例方法,爲了方便演示,我添加了幾個對應的按鈕。
((doc) => {
let n = 0
//獲取目標元素
const target1 = doc.querySelector(".target1")
const target2 = doc.querySelector(".target2")
//添加幾個按鈕方便操作
const observe = doc.querySelector(".observe")
const unobserve = doc.querySelector(".unobserve")
const disconnect = doc.querySelector(".disconnect")
observe.addEventListener('click', () => myObserver.observe(target1))
unobserve.addEventListener('click', () => myObserver.unobserve(target1))
disconnect.addEventListener('click', () => myObserver.disconnect())
//獲取根元素
const root = doc.querySelector(".out-container")
//回調函數
const callback = (entries, observer) => {
n++
console.log(`🚀🚀~ 執行了 ${n} 次callback`);
console.log('🚀🚀~ entries:', entries);
console.log('🚀🚀~ observer:', observer);
};
//配置對象
const options = {
root: root,
rootMargin: '0px 0px 0px 0px',
threshold: [0.1, 0.2, 0.3, 0.5],
trackVisibility: true,
delay: 100
};
//創建觀察器
const myObserver = new IntersectionObserver(callback, options);
//開始監聽目標元素
myObserver.observe(target2);
console.log('🚀🚀~ myObserver:', myObserver);
})(document)
observe
const myObserver = new IntersectionObserver(callback, options);
myObserver.observe(target);
接受一個目標元素作爲參數。很好理解,當我們創建完觀察器實例後,要手動的調用 observe
方法來通知它開始監測目標元素。
可以在同一個觀察者對象中配置監聽多個目標元素
target2
元素是通過代碼自動監測的,而 target1
則是我們在點擊了 observe
按鈕之後開始監測的。通過動圖可以看到,當我單擊 observe
按鈕後,我們的 entries
數組裏面就包含了兩條數據,前文中說到,可以通過 target
屬性來判斷是哪個目標元素。
unobserve
const myObserver = new IntersectionObserver(callback, options);
myObserver.observe(target);
myObserver.unobserve(target)
複製代碼
接收一個目標元素作爲參數,當我們不想監聽某個元素的時候,需要手動調用 unobserve
方法來停止監聽指定目標元素。通過動圖可以發現,當我們點擊 unobserve
按鈕後,由兩條數據變成了一條數據,說明 target1
已經不再接受監測了。
disconnect
const myObserver = new IntersectionObserver(callback, options);
myObserver.disconnect()
當我們不想監測任何一個目標元素時,我們需要手動調用 disconnect
方法停止監聽工作。通過動圖可以看到,當我們點擊 disconnect
按鈕後,控制檯不再輸出 log
,說明監聽工作已經停止,可以通過 observe
再次開啓監聽工作。
takeRecords
返回所有觀察目標的 IntersectionObserverEntry
對象數組,應用場景較少。
當觀察到交互動作發生時,回調函數並不會立即執行,而是在空閒時期使用 requestIdleCallback
來異步執行回調函數,但是也提供了同步調用的 takeRecords
方法。
如果異步的回調先執行了,那麼當我們調用同步的 takeRecords
方法時會返回空數組。同理,如果已經通過 takeRecords
獲取了所有的觀察者實例,那麼回調函數就不會被執行了。
注意事項
構造函數 IntersectionObserver 配置的回調函數都在哪些情況下被調用?
構造函數 IntersectionObserver
配置的回調函數,在以下情況發生時可能會被調用
-
當目標(target)元素與根(root)元素髮生交集的時候執行。
-
兩個元素的相交部分大小發生變化時。
-
Observer
第一次監聽目標元素的時候。
((doc) => {
//回調函數
const callback = () => {
console.log('🚀🚀~ 執行了一次callback');
};
//配置對象
const options = {};
//觀察器實例
const myObserver = new IntersectionObserver(callback, options);
//目標元素
const target = doc.querySelector("#target")
//開始觀察
myObserver.observe(target);
})(document)
可以看到,無論目標元素是否與根元素相交,當我們第一次監聽目標元素的時候,回調函數都會觸發一次,所以不要直接在回調函數里寫邏輯代碼,儘量通過 isIntersecting
或者 intersectionRect
進行判斷之後再執行邏輯代碼。
頁面的可見性如何監測
頁面的可見性可以通過document.visibilityState
或者document.hidden
獲得。頁面可見性的變化可以通過document.visibilitychange
來監聽。
可見性和交叉觀察
當 css
設置了opacity: 0
,visibility: hidden
以及 用其他的元素覆蓋目標元素
,都不會影響交叉觀察器的監測,也就是都不會影響 isIntersecting
屬性的結果,但是會影響 isVisible
屬性的結果, 如果元素設置了 display:none
就不會被檢測了。當然影響元素可視性的屬性不止上述這些,還包括position
,margin
,clip
等等等等... 就靠小夥伴們自行發掘了
交集的計算
所有區域均被 Intersection Observer API
當做一個 矩形 看待。如果元素是不規則的圖形也將會被看成一個包含元素所有區域的最小矩形,相似的,如果元素髮生的交集部分不是一個矩形,那麼也會被看作是一個包含他所有交集區域的最小矩形。
我怎麼知道目標元素來自視口的上方還是下方
目標元素滾動的方向也是可以判斷的,原理是根元素的 entry.rootBounds.y
是固定不變的 ,所以我們只需要計算 entry.boundingClientRect.y
與 entry.rootBounds.y
的大小,當回調函數觸發的時候,我們記錄下當時的位置,如果 entry.boundingClientRect.y < entry.rootBounds.y
,說明是在視口下方,那麼當下一次目標元素可見的時候,我們就知道目標元素時來自視口下方的,反之亦然。
let wasAbove = false;
function callback(entries, observer) {
entries.forEach(entry => {
const isAbove = entry.boundingClientRect.y < entry.rootBounds.y;
if (entry.isIntersecting) {
if (wasAbove) {
// Comes from top
}
}
wasAbove = isAbove;
});
}
應用場景
介紹完基礎知識,總得來幾個實例 (演示代碼採用 VUE3.0),當然實際場景要比這複雜的多,如何在自己的工作學習中應用,還是要靠小夥伴們多多開動聰明的大腦~
數據列表無限滾動
<template>
<div class="box">
<div class="vbody"
v-for='item in list'
:key='item'>內容區域{{item}}</div>
<div class="reference"
ref='reference'></div>
</div>
</template>
<script lang='ts'>
import { defineComponent, onMounted, reactive, ref } from 'vue'
export default defineComponent({
name: '',
setup() {
const reference = ref(null)
const list = reactive([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
onMounted(() => {
let n = 10
//回調函數
const callback = (entries) => {
const myEntry = entries[0]
if (myEntry.isIntersecting) {
console.log(`🚀🚀~ 觸發了無線滾動,開始模擬請求數據 ${n}`)
n++
list.push(n)
}
}
//配置對象
const options = {
root: null,
rootMargin: '0px 0px 0px 0px',
threshold: [0, 1],
trackVisibility: true,
delay: 100,
}
//觀察器實例
const myObserver = new IntersectionObserver(callback, options)
//開始觀察
myObserver.observe(reference.value)
})
return { reference, list }
},
})
</script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.reference {
width: 100%;
visibility: hidden;
}
.vbody {
width: 100%;
height: 200px;
background-color: red;
color: aliceblue;
font-size: 40px;
text-align: center;
line-height: 200px;
margin: 10px 0;
}
</style>
我們只需要在底部添加一個參考元素,當參考元素可見時,就向後臺請求數據,就可以實現無線滾動的效果了。
圖片預加載
<template>
<div class="box">
<div class="vbody">內容區域</div>
<div class="vbody">內容區域</div>
<div class="header"
ref='header'>
<img :src="url">
</div>
<div class="vbody">內容區域</div>
</div>
</template>
<script lang='ts'>
import { defineComponent, onMounted, ref } from 'vue'
export default defineComponent({
name: '',
setup() {
const header = ref(null)
const url = ref('')
onMounted(() => {
//回調函數
const callback = (entries) => {
const myEntry = entries[0]
if (myEntry.isIntersecting) {
console.log('🚀🚀~ 預加載圖片開始')
url.value =
'//img10.360buyimg.com/imgzone/jfs/t1/197235/15/2956/67824/6115e076Ede17a418/d1350d4d5e52ef50.jpg'
}
}
//配置對象
const options = {
root: null,
rootMargin: '200px 200px 200px 200px',
threshold: [0],
trackVisibility: true,
delay: 100,
}
//觀察器實例
const myObserver = new IntersectionObserver(callback, options)
//開始觀察
myObserver.observe(header.value)
})
return { header, url }
},
})
</script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.box {
}
.header {
width: 100%;
height: 400px;
background-color: blue;
color: aliceblue;
font-size: 40px;
text-align: center;
line-height: 400px;
}
.header img {
width: 100%;
height: 100%;
}
.reference {
width: 100%;
visibility: hidden;
}
.vbody {
width: 100%;
height: 800px;
background-color: red;
color: aliceblue;
font-size: 40px;
text-align: center;
line-height: 800px;
margin: 10px 0;
}
</style>
利用 options
的 rootMargin
屬性,可以在圖片即將進入可視區域的時間進行圖片的加載,即避免了提前請求大量圖片造成的性能問題,也避免了圖片進入窗口才加載已經來不及的問題。
吸頂
<template>
<div class="box">
<div class="reference"
ref='reference'></div>
<div class="header"
ref='header'>吸頂區域</div>
<div class="vbody">內容區域</div>
<div class="vbody">內容區域</div>
<div class="vbody">內容區域</div>
</div>
</template>
<script lang='ts'>
import { defineComponent, onMounted, ref } from 'vue'
export default defineComponent({
name: '',
setup() {
const header = ref(null)
const reference = ref(null)
onMounted(() => {
//回調函數
const callback = (entries) => {
const myEntry = entries[0]
if (!myEntry.isIntersecting) {
console.log('🚀🚀~ 觸發了吸頂')
header.value.style.position = 'fixed'
header.value.style.top = '0px'
} else {
console.log('🚀🚀~ 取消吸頂')
header.value.style.position = 'relative'
}
}
//配置對象
const options = {
root: null,
rootMargin: '0px 0px 0px 0px',
threshold: [0, 1],
trackVisibility: true,
delay: 100,
}
//觀察器實例
const myObserver = new IntersectionObserver(callback, options)
//開始觀察
myObserver.observe(reference.value)
})
return { reference, header }
},
})
</script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.header {
width: 100%;
height: 100px;
background-color: blue;
color: aliceblue;
font-size: 40px;
text-align: center;
line-height: 100px;
}
.reference {
width: 100%;
visibility: hidden;
}
.vbody {
width: 100%;
height: 800px;
background-color: red;
color: aliceblue;
font-size: 40px;
text-align: center;
line-height: 800px;
margin: 10px 0;
}
</style>
思路就是利用一個參考元素作爲交叉觀察器觀察的對象,當參考元素可見的時候,取消吸頂區域的 fixed
屬性,否則添加 fixed
屬性,吸底稍微複雜一點,但是道理差不多,留給小夥伴們自行研究吧 ~ ~。
埋點上報
<template>
<div class="box">
<div class="vbody">內容區域</div>
<div class="vbody">內容區域</div>
<div class="header"
ref='header'>埋點區域</div>
<div class="vbody">內容區域</div>
</div>
</template>
<script lang='ts'>
import { defineComponent, onMounted, ref } from 'vue'
export default defineComponent({
name: '',
setup() {
const header = ref(null)
onMounted(() => {
//回調函數
const callback = (entries) => {
const myEntry = entries[0]
if (myEntry.isIntersecting) {
console.log('🚀🚀~ 觸發了埋點')
}
}
//配置對象
const options = {
root: null,
rootMargin: '0px 0px 0px 0px',
threshold: [0.5],
trackVisibility: true,
delay: 100,
}
//觀察器實例
const myObserver = new IntersectionObserver(callback, options)
//開始觀察
myObserver.observe(header.value)
})
return { header }
},
})
</script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.header {
width: 100%;
height: 400px;
background-color: blue;
color: aliceblue;
font-size: 40px;
text-align: center;
line-height: 400px;
}
.vbody {
width: 100%;
height: 800px;
background-color: red;
color: aliceblue;
font-size: 40px;
text-align: center;
line-height: 800px;
margin: 10px 0;
}
</style>
通常情況下,我們統計一個元素是否被用戶有效的看到,並不是元素剛出現就觸發埋點,而是元素進入可是區域一定比例纔可以,我們可以配置 options
的 threshold
爲 0.5
。
等等等等。。。。
這個 api
可以說是非常強大了,可玩性也是極高,大家自由發揮 ~ ~
兼容性
IntersectionObserver V2
的。所以小夥伴們在用的時候一定要注意兼容性。當然也有兼容解決方案,那就是 intersection-observer-polyfill
參考資料
[1] Can I Use:
https://caniuse.com/?search=IntersectionObserver%20
[2] MDN Intersection Observer:
https://developer.mozilla.org/zh-CN/docs/Web/API/IntersectionObserver
[3] IntersectionObserver API 使用教程:
https://www.ruanyifeng.com/blog/2016/11/intersectionobserver_api.html
[4] intersection-observer-polyfill:
https://www.npmjs.com/package/intersection-observer-polyfill
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/fu6VqPfWn7mB7evopnDeLA