一文詳解前端進階之IntersectionObserver
背景介紹
作為一款產(chǎn)品,往往希望能得到用戶的反饋,從而通過對用戶行為的分析進行功能、交互等方面的改進。然而直接的一對一的用戶交流是低效且困難的,因此最普遍的做法便是通過數(shù)據(jù)埋點來反推用戶的行為。那么數(shù)據(jù)埋點中很重要的一環(huán)便是:曝光。
所謂曝光,便是頁面被展示的時候進行打點。舉個簡單的例子:用戶進入分類頁面,商品以行為單位從上而下進行排列。當用戶滾動頁面時,之前不在視窗范圍內(nèi)的商品就會出現(xiàn),此時,這部分商品就算曝光了,需要進行一次記錄。
那么為了實現(xiàn)上面功能,最普遍的做法有兩個。其一:監(jiān)聽滾動事件,然后計算某個商品與視窗的相對位置,從而判斷是否可見。其二:設置一個定時器,然后以固定的時間為間隔計算某個商品與視窗的相對位置。
上面兩種做法在某種程度上能夠?qū)崿F(xiàn)我們的目的,但是會有一些問題,比如最明顯的:慢。因為計算相對位置時會調(diào)用getBoundingClientRect(),這個api會導致瀏覽器進行全頁面的重新布局,影響性能,特別是在頻繁進行時。因此IntersectionObserver API進入了我們的視野。
IntersectionObserver API介紹
關(guān)于IntersectionObserver API的官方文檔見此。兼容性如下圖所示:

簡單的說IntersectionObserver讓你知道什么時候observe的元素進入或者存在在root區(qū)域里了。下面我們來看下這個API的具體內(nèi)容:
// 用構(gòu)造函數(shù)生成觀察者實例,回調(diào)函數(shù)是必須的,后面的配置對象是可選的
const observer = new IntersectionObserver(changes => {
for (const change of changes) {
console.log(change.time); // 相交發(fā)生時經(jīng)過的時間
console.log(change.rootBounds); // 表示發(fā)生相交時根元素可見區(qū)域的矩形信息,是一個對象值
console.log(change.boundingClientRect); // target.boundingClientRect()發(fā)生相交時目標元素的矩形信息,也是個對象值
console.log(change.intersectionRect); // 根元素與目標元素相交時的矩形信息
console.log(change.intersectionRatio); // 表示相交區(qū)域占目標區(qū)域的百分比,是一個0到1的值
console.log(change.target); // 相交發(fā)生時的目標元素
}
}, {
root: null,
threshold: [0, 0.5, 1],
rootMargin: "50px"
});
// 實例屬性
observer.root
observer.rootMargin
observer.thresholds
// 實例方法
observer.observe(target); // 觀察針對某個特定元素的相交事件
observer.unobserve(target); // 停止對某個特定元素的相交事件的觀察
observer.disconnect(); // 停止對所有目標元素的閾值事件的觀察,簡單的說就是停用整個IntersectionObserver
// 除了上面三個實例方法,還有一個takeRecords()的方法,之后會詳細介紹
IntersectionObserver API允許開發(fā)人員了解目標dom元素相對于intersection root的可見性。這個root可以通過實例屬性獲取。默認情況下它是null,此時它不是真正意義上的元素,它指視窗范圍,因此只要視窗范圍內(nèi)的目標元素滾入視窗時,就會觸發(fā)回調(diào)函數(shù)(如果root元素不存在了,則執(zhí)行其任何的observe都會出錯)。
我們可以在配置對象中將root改為具體的元素,此時當目標元素出現(xiàn)在root元素中時會觸發(fā)回調(diào),注意,在這種情況下相交可能發(fā)生在視窗下面。具體代碼在下,感興趣的同學可以試一下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>intersectionObserve</title>
<style type="text/css">
#root {
position: relative;
width: 400px;
height: calc(100vh + 200px);
background: lightblue;
overflow: scroll;
}
#target {
position: absolute;
top: calc(100vh + 800px);
width: 100px;
height: 100px;
background: red;
}
</style>
</head>
<body>
<div id="root">
<div id="target"></div>
</div>
<script type="text/javascript">
let ele = new IntersectionObserver(
(entries) => {
console.log(entries);
}, {
root: root
}
);
ele.observe(target);
</script>
</body>
</html>
在上面的例子中,回調(diào)函數(shù)打印出來的對象中有一個intersectionRatio值,這個值其實涉及到了整個API的核心功能:當目標元素和根元素相交的面積占目標元素面積的百分比到達或跨過某些指定的臨界值時就會觸發(fā)回調(diào)函數(shù)。因此相對的在配置對象里有一個threshold來對這個百分比進行配置,默認情況下這個值是[0],注意里面的值不能在0-1之外,否則會報錯。我們舉個例子如下:
let ele = new IntersectionObserver(
(entries) => {
console.log(entries);
}, {
threshold: [0, 0.5, 1.0]
}
);
ele.observe(target);
在上面這個例子中,我們設定了0,0.5,1.0這三個值,因此當交叉區(qū)域跨越0,0.5,1.0時都會觸發(fā)回調(diào)函數(shù)。注意我這邊的用詞是跨越,而不是到達。因為會存在以下兩種情況導致回調(diào)打印出來的intersectionRatio不為0,0.5和1.0。
一、瀏覽器對相交的檢測是有時間間隔的。瀏覽器的渲染工作都是以幀為單位的,而IntersectionObserver是發(fā)生在幀里面的。因此假如你設定了[0,0.1,0.2,0.3,0.4,0.5]這個threshold,但是你的滾動過程特別快,導致所有的繪制在一幀里面結(jié)束了,此時回調(diào)只會挑最近的臨界值觸發(fā)一次。
二、 IntersectionObserver是異步的。在瀏覽器內(nèi)部,當一個觀察者實例觀察到眾多的相交行為時,它不會立即執(zhí)行。關(guān)于IntersectionObserver的草案里面寫明了其實現(xiàn)是基于requestIdleCallback()來異步的執(zhí)行我們的回調(diào)函數(shù)的,并且規(guī)定了最大的延遲時間是100ms。關(guān)于這部分涉及到前面第一段代碼里的一個實例方法takeRecords()。如果你很迫切的希望馬上知道是否有相交,你不希望等待可能的100ms,此時你就能調(diào)用takeRecords(),此后你能馬上獲得包含IntersectionObserverEntry 對象的數(shù)組,里面有相交信息,如果沒有任何相交行為發(fā)生,則返回一個空數(shù)組。但這個方法與正常的異步回調(diào)是互斥的,如果它先執(zhí)行了則正?;卣{(diào)里面就沒信息了,反之亦然。
除開上面的問題,如果目標元素的面積為0會產(chǎn)生什么情況呢?因為與0計算相交率是沒有意義的,實際我們舉個例子:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>intersectionObserve</title>
<style type="text/css">
#target {
position: relative;
top: calc(100vh + 500px);
width: 100px;
height: 100px;
background: red;
}
</style>
</head>
<body>
<div id="target"></div>
<div id="img"></div>
<script type="text/javascript">
let ele = new IntersectionObserver(
(entries) => {
console.log(entries);
}, {
threshold: [0, 0.5, 1.0]
}
);
ele.observe(img);
</script>
</body>
</html>
我們會看到,雖然我們設定了0.5這個閾值,但實際回調(diào)只會在0與1.0時觸發(fā)。這是一種特殊的處理方式。
這里需要強調(diào)一點的是,我們的目標元素在Observe的時候可以不存在的(注意這里的不存在是指沒有插入dom結(jié)構(gòu),但是元素本身是需要存在的),只需要在相交發(fā)生時存在就行了,我們來舉個栗子:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>intersectionObserve</title>
<style type="text/css">
#target {
position: relative;
top: calc(100vh + 500px);
width: 100px;
height: 100px;
background: red;
}
</style>
</head>
<body>
<div id="target"></div>
<script type="text/javascript">
let ele = new IntersectionObserver(
(entries) => {
console.log(entries);
}, {
threshold: [0, 0.5, 1.0]
}
);
let img = document.createElement('div');
ele.observe(img);
setTimeout(() => {
document.body.appendChild(img);
}, 5000);
</script>
</body>
</html>
同理,如果目標元素與根元素處于相交狀態(tài),但是在一段時間后目標元素不存在了(比如remove,或者display:none)了,那么此時依然會觸發(fā)一次回調(diào)。但是如果本身就不處于相交狀態(tài),然后消失掉了,因為0->0沒有變化,所以不會觸發(fā)回調(diào),具體如下面的例子所示:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>intersectionObserve</title>
<style type="text/css">
#target {
position: relative;
top: calc(100vh + 500px);
width: 100px;
height: 100px;
background: red;
}
</style>
</head>
<body>
<div id="target"></div>
<script type="text/javascript">
let ele = new IntersectionObserver(
(entries) => {
console.log(entries);
}
);
ele.observe(target);
setTimeout(() => {
document.body.removeChild(target);
}, 5000);
</script>
</body>
</html>
IntersectionObserver API與iframe
互聯(lián)網(wǎng)上的很多小廣告都是通過iframe嵌入的,然而現(xiàn)有的情況下很難獲取iframe在頂層視窗內(nèi)的曝光,但是使用IntersectionObserver API我們卻可以做到這點。下面舉個例子:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>intersectionObserve</title>
<style type="text/css">
#root {
position: relative;
top: calc(100vh + 800px);
width: 100px;
height: 100px;
}
#iframe {
width: 600px;
height: 600px;
margin-bottom: 300px;
}
</style>
</head>
<body>
<div id="root">
<iframe id="iframe"></iframe>
</div>
<script>
let iframeTemplate = `
<div id="target"><p>i am iframe</p></div>
<style>
#target {
width: 500px;
height: 500px;
background: red;
}
#target p {
font-size: 90px;
}
</style>
<script>
let observer = new IntersectionObserver((entries) => {
console.log(entries)
}, {
threshold: [0,0.5,1.0]
})
observer.observe(target)
</script>`
iframe.src = URL.createObjectURL(new Blob([iframeTemplate], {"type": "text/html"}))
</script>
</body>
</html>
從上面的例子可以看出,使用此API不僅能夠使iframe在視窗內(nèi)出現(xiàn)時觸發(fā)回調(diào),而且threshold值同樣能夠起作用。這樣一來,大大簡化了此類情況下獲取曝光的難度。
延遲加載與無限滾動
上面我們關(guān)于配置參數(shù)已經(jīng)提到了root和threshold,實際上還有一個值:rootMargin。這個值實際就是給根元素添加了一個假想的margin值。使用場景最普遍的是用于延遲加載。因為如果真的等目標元素與根元素相交的時候再進行加載圖片等功能就已經(jīng)晚了,所以有一個rootMargin值,這樣等于根元素延伸開去了,目標元素只要與延伸部分相交就會觸發(fā)回調(diào),下面我們來繼續(xù)舉個例子:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>intersectionObserve</title>
<style type="text/css">
#root {
width: 500px;
height: 800px;
overflow: scroll;
background-color: pink;
}
#target {
position: relative;
top: calc(100vh + 500px);
width: 100px;
height: 100px;
background: red;
}
</style>
</head>
<body>
<div id="root">
<div id="target"></div>
</div>
<script type="text/javascript">
let ele = new IntersectionObserver(
(entries) => {
console.log(entries);
}, {
rootMargin: '100px',
root: root
}
);
ele.observe(target);
</script>
</body>
</html>
在上面的例子中,目標元素并沒有出現(xiàn)在根元素的視窗里的時候就已經(jīng)觸發(fā)回調(diào)了。
整個API可以用來實現(xiàn)無限滾動和延遲加載,下面就分別舉出兩個簡單的例子來啟發(fā)思路。
延遲加載的例子:
<!DOCTYPE html> <html lang="en">
<head>
<meta charset="utf-8" />
<title>intersectionObserve</title>
<style type="text/css">
.img {
height: 1000px;
overflow-y: hidden;
}
</style>
</head>
<body>
<ul>
<li class="img">
<img src="" class="img-item" data-src="http://okzzg7ifm.bkt.clouddn.com/cat.png"/>
</li>
<li class="img">
<img src="" class="img-item" data-src="http://okzzg7ifm.bkt.clouddn.com/01.png"/>
</li>
<li class="img">
<img src="" class="img-item" data-src="http://okzzg7ifm.bkt.clouddn.com/virtualdom.png"/>
</li>
<li class="img">
<img src="" class="img-item" data-src="http://okzzg7ifm.bkt.clouddn.com/reactlife.png"/>
</li>
</ul>
<script type="text/javascript">
let ele = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.intersectionRatio > 0) {
entry.target.src = entry.target.dataset.src;
}
})
}, {
rootMargin: '100px',
threshold: [0.000001]
}
);
let eleArray = Array.from(document.getElementsByClassName('img-item'));
eleArray.forEach((item) => {
ele.observe(item);
})
</script>
</body>
</html>
無限滾動的例子:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>intersectionObserve</title>
<style type="text/css">
.img {
height: 1200px;
overflow: hidden;
}
#flag {
height: 20px;
background-color: pink;
}
</style>
</head>
<body>
<ul id="imgContainer">
<li class="img">
<img src="http://okzzg7ifm.bkt.clouddn.com/cat.png"/>
</li>
<li class="img">
<img src="http://okzzg7ifm.bkt.clouddn.com/01.png"/>
</li>
<li class="img">
<img src="http://okzzg7ifm.bkt.clouddn.com/virtualdom.png"/>
</li>
<li class="img">
<img src="http://okzzg7ifm.bkt.clouddn.com/reactlife.png"/>
</li>
</ul>
<div id="flag"></div>
<script type="text/javascript">
let imgList = [
'http://okzzg7ifm.bkt.clouddn.com/immutable-coperation.png', 'http://okzzg7ifm.bkt.clouddn.com/flexdirection.png', 'http://okzzg7ifm.bkt.clouddn.com/immutable-exampleLayout.png'
]
let ele = new IntersectionObserver(
(entries) => {
if (entries[0].intersectionRatio > 0) {
if (imgList.length) {
let newImgli = document.createElement('li');
newImgli.setAttribute("class", "img");
let newImg = document.createElement('img');
newImg.setAttribute("src", imgList[0]);
newImgli.appendChild(newImg);
document.getElementById('imgContainer').appendChild(newImgli);
imgList.shift();
}
}
}, {
rootMargin: '100px',
threshold: [0.000001]
}
);
ele.observe(flag);
</script>
</body>
</html>
通篇看下來大家是不是感覺這個API還是很好玩的,api已經(jīng)問世很多年了,大部分瀏覽器都可以兼容,低版本瀏覽器可以通過Polyfill解決,規(guī)范制訂者在github上發(fā)布了Polyfill。
利弊介紹
優(yōu)點
- 性能比直接的監(jiān)聽scroll事件或者設置timer都好
- 使用簡單
- 利用它的功能組合可以實現(xiàn)很多其他效果,比如無限滾動等
- 對iframe的支持好
缺點
- 它不是完美像素與無延遲的,畢竟根本上是異步的。因此不適合做滾動動畫
以上就是一文詳解前端進階之IntersectionObserver的詳細內(nèi)容,更多關(guān)于前端IntersectionObserver的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
詳解JavaScript中Hash Map映射結(jié)構(gòu)的實現(xiàn)
Hash Map通常在JavaScript中作為一個簡單的來存儲鍵值對的地方,不過哈希對象Object并不是一個真正的哈希映射,沒Java中的Hash Map來的那么強大,well,接下來帶大家詳解JavaScript中Hash Map映射結(jié)構(gòu)的實現(xiàn)2016-05-05
JS實現(xiàn)顯示帶倒影的圖片橫排居中放大展示特效實例【測試可用】
這篇文章主要介紹了JS實現(xiàn)顯示帶倒影的圖片橫排居中放大展示功能,可實現(xiàn)點擊圖片及點擊左右按鈕滑動切換的效果,涉及javascript針對鼠標事件的響應及頁面元素動態(tài)操作相關(guān)技巧,需要的朋友可以參考下2016-08-08

