欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

IntersectionObserver API 詳解篇

 更新時間:2016年12月11日 20:57:00   投稿:mdxy-dxy  
這篇文章主要介紹了IntersectionObserver API 詳解篇,需要的朋友可以參考下

溫馨提示:本文目前僅適用于在 Chrome 51 及以上中瀏覽。

2016.11.1 追加,F(xiàn)irefox 52 也已經(jīng)實現(xiàn)。

2016.11.29 追加,F(xiàn)irefox 的人擔心目前規(guī)范不夠穩(wěn)定,未來很難保證向后兼容,所以禁用了這個 API,需要手動打開 dom.IntersectionObserver.enabled 才行。

IntersectionObserver API 是用來監(jiān)視某個元素是否滾動進了瀏覽器窗口的可視區(qū)域(視口)或者滾動進了它的某個祖先元素的可視區(qū)域內(nèi)。它的主要功能是用來實現(xiàn)延遲加載和展現(xiàn)量統(tǒng)計。先來看一段視頻簡介:

再來看看名字,名字里第一個單詞 intersection 是交集的意思,小時候數(shù)學里面就學過:

不過在網(wǎng)頁里,元素都是矩形的:

第二個單詞 observer 是觀察者的意思,和 MutationObserver 以及已死的 Object.observe 中的 observe(r) 一個意思。

下面列出了這個 API 中所有的參數(shù)、屬性、方法:

// 用構(gòu)造函數(shù)生成觀察者實例
let observer = new IntersectionObserver((entries, observer) => {
 // 回調(diào)函數(shù)中可以拿到每次相交發(fā)生時所產(chǎn)生的交集的信息
 for (let entry of entries) {
 console.log(entry.time)
 console.log(entry.target)
 console.log(entry.rootBounds)
 console.log(entry.boundingClientRect
 console.log(entry.intersectionRect)
 console.log(entry.intersectionRatio)
 }
}, { // 構(gòu)造函數(shù)的選項
 root: null,
 threshold: [0, 0.5, 1],
 rootMargin: "50px, 0px"
})

// 實例屬性
observer.root
observer.rootMargin
observer.thresholds

// 實例方法
observer.observe()
observer.unobserve()
observer.disconnect()
observer.takeRecords()

然后分三小節(jié)詳細介紹它們:

構(gòu)造函數(shù)

new IntersectionObserver(callback, options)

callback 是個必選參數(shù),當有相交發(fā)生時,瀏覽器便會調(diào)用它,后面會詳細介紹;options 整個參數(shù)對象以及它的三個屬性都是可選的:

root

IntersectionObserver API 的適用場景主要是這樣的:一個可以滾動的元素,我們叫它根元素,它有很多后代元素,想要做的就是判斷它的某個后代元素是否滾動進了自己的可視區(qū)域范圍。這個 root 參數(shù)就是用來指定根元素的,默認值是 null。

如果它的值是 null,根元素就不是個真正意義上的元素了,而是這個瀏覽器窗口了,可以理解成 window,但 window 也不是元素(甚至不是節(jié)點)。這時當前窗口里的所有元素,都可以理解成是 null 根元素的后代元素,都是可以被觀察的。

下面這個 demo 演示了根元素為 null 的用法:

<div id="info">我藏在頁面底部,請向下滾動</div>
<div id="target"></div>

<style>
 #info {
 position: fixed;
 }

 #target {
 position: absolute;
 top: calc(100vh + 500px);
 width: 100px;
 height: 100px;
 background: red;
 }
</style>

<script>
 let observer = new IntersectionObserver(() => {
 if (!target.isIntersecting) {
 info.textContent = "我出來了"
 target.isIntersecting = true
 } else {
 info.textContent = "我藏在頁面底部,請向下滾動"
 target.isIntersecting = false
 }
 }, {
 root: null // null 的時候可以省略
 })

 observer.observe(target)
</script>

需要注意的是,這里我通過在 target 上添加了個叫 isIntersecting 的屬性來判斷它是進來還是離開了,為什么這么做?先忽略掉,下面會有一小節(jié)專門解釋。

根元素除了是 null,還可以是目標元素任意的祖先元素:

<div id="root">
 <div id="info">向下滾動就能看到我</div>
 <div id="target"></div>
</div>

<style>
 #root {
 position: relative;
 width: 200px;
 height: 100vh;
 margin: 0 auto;
 overflow: scroll;
 border: 1px solid #ccc;
 }
 
 #info {
 position: fixed;
 }
 
 #target {
 position: absolute;
 top: calc(100vh + 500px);
 width: 100px;
 height: 100px;
 background: red;
 }
</style>

<script>
 let observer = new IntersectionObserver(() => {
 if (!target.isIntersecting) {
 info.textContent = "我出來了"
 target.isIntersecting = true
 } else {
 info.textContent = "向下滾動就能看到我"
 target.isIntersecting = false
 }
 }, {
 root: root
 })

 observer.observe(target)
</script>

需要注意的一點是,如果 root 不是 null,那么相交區(qū)域就不一定在視口內(nèi)了,因為 root 和 target 的相交也可能發(fā)生在視口下方,像下面這個 demo 所演示的:

<div id="root">
 <div id="info">慢慢向下滾動</div>
 <div id="target"></div>
</div>

<style>
 #root {
 position: relative;
 width: 200px;
 height: calc(100vh + 500px);
 margin: 0 auto;
 overflow: scroll;
 border: 1px solid #ccc;
 }
 
 #info {
 position: fixed;
 }
 
 #target {
 position: absolute;
 top: calc(100vh + 1000px);
 width: 100px;
 height: 100px;
 background: red;
 }
</style>

<script>
 let observer = new IntersectionObserver(() => {
 if (!target.isIntersecting) {
 info.textContent = "我和 root 相交了,但你還是看不見"
 target.isIntersecting = true
 } else {
 info.textContent = "慢慢向下滾動"
 target.isIntersecting = false
 }
 }, {
 root: root
 })

 observer.observe(target)
</script>

總結(jié)一下:這一小節(jié)我們講了根元素的兩種類型,null 和任意的祖先元素,其中 null 值表示根元素為當前窗口(的視口)。

threshold

當目標元素和根元素相交時,用相交的面積除以目標元素的面積會得到一個 0 到 1(0% 到 100%)的數(shù)值:

下面這句話很重要,IntersectionObserver API 的基本工作原理就是:當目標元素和根元素相交的面積占目標元素面積的百分比到達或跨過某些指定的臨界值時就會觸發(fā)回調(diào)函數(shù)。threshold 參數(shù)就是用來指定那個臨界值的,默認值是 0,表示倆元素剛剛挨上就觸發(fā)回調(diào)。有效的臨界值可以是在 0 到 1 閉區(qū)間內(nèi)的任意數(shù)值,比如 0.5 表示當相交面積占目標元素面積的一半時觸發(fā)回調(diào)。而且可以指定多個臨界值,用數(shù)組形式,比如 [0, 0.5, 1],表示在兩個矩形開始相交,相交一半,完全相交這三個時刻都要觸發(fā)一次回調(diào)函數(shù)。如果你傳了個空數(shù)組,它會給你自動插入 0,變成 [0],也等效于默認值 0。

<

下面的動畫演示了當 threshold 參數(shù)為 [0, 0.5, 1] 時,向下滾動頁面時回調(diào)函數(shù)是在何時觸發(fā)的:

不僅當目標元素從視口外移動到視口內(nèi)時會觸發(fā)回調(diào),從視口內(nèi)移動到視口外也會:

你可以在這個 demo 里驗證上面的兩個動畫:

你可以在這個 demo 里驗證上面的兩個動畫:

<div id="info">
 慢慢向下滾動,相交次數(shù):
 <span id="times">0</span>
</div>
<div id="target"></div>

<style>
 #info {
 position: fixed;
 }
 
 #target {
 position: absolute;
 top: 200%;
 width: 100px;
 height: 100px;
 background: red;
 margin-bottom: 100px;
 }
</style>

<script>
 let observer = new IntersectionObserver(() => {
 times.textContent = +times.textContent + 1
 }, {
 threshold: [0, 0.5, 1]
 })

 observer.observe(target)
</script>

threshold 數(shù)組里的數(shù)字的順序沒有強硬要求,為了可讀性,最好從小到大書寫。如果指定的某個臨界值小于 0 或者大于 1,瀏覽器會報錯:

<script>
new IntersectionObserver(() => {}, {
 threshold: 2 // SyntaxError: Failed to construct 'Intersection': Threshold values must be between 0 and 1.
})
</script> 

rootMagin

本文一開始就說了,這個 API 的主要用途之一就是用來實現(xiàn)延遲加載,那么真正的延遲加載會等 img 標簽或者其它類型的目標區(qū)塊進入視口才執(zhí)行加載動作嗎?顯然,那就太遲了。我們通常都會提前幾百像素預先加載,rootMargin 就是用來干這個的。rootMargin 可以給根元素添加一個假想的 margin,從而對真實的根元素區(qū)域進行縮放。比如當 root 為 null 時設(shè)置 rootMargin: "100px",實際的根元素矩形四條邊都會被放大 100px,像這樣:

效果可以想象到,如果 threshold 為 0,那么當目標元素距離視口 100px 的時候(無論哪個方向),回調(diào)函數(shù)就提前觸發(fā)了??紤]到常見的頁面都沒有橫向滾動的需求,rootMargin 參數(shù)的值一般都是 "100px 0px",這種形式,也就是左右 margin 一般都是 0px. 下面是一個用 IntersectionObserver 實現(xiàn)圖片在距視口 500px 的時候延遲加載的 demo:

<div id="info">圖片在頁面底部,仍未加載,請向下滾動</div>
<img id="img" src="data:image/webp;base64,UklGRiQAAABXRUJQVlA4IBgAAAAwAQCdASoBAAEAAwA0JaQAA3AA/vuUAAA="
  data-src="https://img.alicdn.com/bao/uploaded/i7/TB1BUK4MpXXXXa1XpXXYXGcGpXX_M2.SS2">

<style>
 #info {
 position: fixed;
 }

 #img {
 position: absolute;
 top: 300%;
 }
</style>

<script>
 let observer = new IntersectionObserver(() => {
 observer.unobserve(img)
 info.textContent = "開始加載圖片!"
 img.src = img.dataset.src
 }, {
 rootMargin: "500px 0px"
 })

 observer.observe(img)
</script>

注意 rootMargin 的值雖然和 CSS 里 margin 的值的格式一樣,但存在一些限制,rootMargin 只能用 px 和百分比兩種單位,用其它的單位會報錯,比如用 em:

<script>
new IntersectionObserver(() => {}, {
 rootMargin: "10em" // SyntaxError: Failed to construct 'Intersection': rootMargin must be specified in pixels or percent.
})
</script>

rootMargin 用百分比的話就是相對根元素的真實尺寸的百分比了,比如 rootMargin: "0px 0px 50% 0px",表示根元素的尺寸向下擴大了 50%。

如果使用了負 margin,真實的根元素區(qū)域會被縮小,對應的延遲加載就會延后,比如用了 rootMargin: "-100px" 的話,目標元素滾動進根元素可視區(qū)域內(nèi)部 100px 的時候才有可能觸發(fā)回調(diào)。

實例

實例屬性

root

該觀察者實例的根元素(默認值為 null):

new IntersectionObserver(() => {}).root // null
new IntersectionObserver(() => {}, {root: document.body}).root // document.body

rootMargin

rootMargin 參數(shù)(默認值為 "0px")經(jīng)過序列化后的值:

new IntersectionObserver(() => {}).rootMargin // "0px 0px 0px 0px"
new IntersectionObserver(() => {}, {rootMargin: "50px"}).rootMargin // "50px 50px 50px 50px"
new IntersectionObserver(() => {}, {rootMargin: "50% 0px"}).rootMargin // "50% 0px 50% 0px"
new IntersectionObserver(() => {}, {rootMargin: "50% 0px 50px"}).rootMargin // 50% 0px 50px 0px" 
new IntersectionObserver(() => {}, {rootMargin: "1px 2px 3px 4px"}).rootMargin // "1px 2px 3px 4px"

thresholds

threshold 參數(shù)(默認值為 0)經(jīng)過序列化后的值,即便你傳入的是一個數(shù)字,序列化后也是個數(shù)組,目前 Chrome 的實現(xiàn)里數(shù)字的精度會有丟失,但無礙:

new IntersectionObserver(() => {}).thresholds // [0]
new IntersectionObserver(() => {}, {threshold: 1}).thresholds // [1]
new IntersectionObserver(() => {}, {threshold: [0.3, 0.6]}).thresholds // [[0.30000001192092896, 0.6000000238418579]]
Object.isFrozen(new IntersectionObserver(() => {}).thresholds) // true, 是個被 freeze 過的數(shù)組

這三個實例屬性都是用來標識一個觀察者實例的,都是讓人來讀的,在代碼中沒有太大用途。

實例方法

observe()

觀察某個目標元素,一個觀察者實例可以觀察任意多個目標元素。注意,這里可能有同學會問:能不能 delegate?能不能只調(diào)用一次 observe 方法就能觀察一個頁面里的所有 img 元素,甚至那些未產(chǎn)生的?答案是不能,這不是事件,沒有冒泡。

unobserve()

取消對某個目標元素的觀察,延遲加載通常都是一次性的,observe 的回調(diào)里應該直接調(diào)用 unobserve() 那個元素.

disconnect()

取消觀察所有已觀察的目標元素

takeRecords()

理解這個方法需要講點底層的東西:在瀏覽器內(nèi)部,當一個觀察者實例在某一時刻觀察到了若干個相交動作時,它不會立即執(zhí)行回調(diào),它會調(diào)用 window.requestIdleCallback() (目前只有 Chrome 支持)來異步的執(zhí)行我們指定的回調(diào)函數(shù),而且還規(guī)定了最大的延遲時間是 100 毫秒,相當于瀏覽器會執(zhí)行:

requestIdleCallback(() => {
 if (entries.length > 0) {
 callback(entries, observer)
 }
}, {
 timeout: 100
})

你的回調(diào)可能在隨后 1 毫秒內(nèi)就執(zhí)行,也可能在第 100 毫秒才執(zhí)行,這是不確定的。在這不確定的 100 毫秒之間的某一刻,假如你迫切需要知道這個觀察者實例有沒有觀察到相交動作,你就得調(diào)用 takeRecords() 方法,它會同步返回包含若干個 IntersectionObserverEntry 對象的數(shù)組(IntersectionObserverEntry 對象包含每次相交的信息,在下節(jié)講),如果該觀察者實例此刻并沒有觀察到相交動作,那它就返回個空數(shù)組。

注意,對于同一個相交信息來說,同步的 takeRecords() 和異步的回調(diào)函數(shù)是互斥的,如果回調(diào)先執(zhí)行了,那么你手動調(diào)用 takeRecords() 就必然會拿到空數(shù)組,如果你已經(jīng)通過 takeRecords() 拿到那個相交信息了,那么你指定的回調(diào)就不會被執(zhí)行了(entries.length > 0 是 false)。

這個方法的真實使用場景很少,我舉不出來,我只能寫出一個驗證上面兩段話(時序無規(guī)律)的測試代碼:

<script>
 setInterval(() => {
 let observer = new IntersectionObserver(entries => {
 if (entries.length) {
 document.body.innerHTML += "<p>異步的 requestIdleCallback() 回調(diào)先執(zhí)行了"
 }
 })

 requestAnimationFrame(() => {
 setTimeout(() => {
 if (observer.takeRecords().length) {
  document.body.innerHTML += "<p>同步的 takeRecords() 先執(zhí)行了"
 }
 }, 0)
 })

 observer.observe(document.body)

 scrollTo(0, 1e10)
 }, 100)
</script>

回調(diào)函數(shù)

new IntersectionObserver(function(entries, observer) {
 for (let entry of entries) {
 console.log(entry.time)
 console.log(entry.target)
 console.log(entry.rootBounds)
 console.log(entry.boundingClientRect
 console.log(entry.intersectionRect)
 console.log(entry.intersectionRatio)
 }
})

回調(diào)函數(shù)共有兩個參數(shù),第二個參數(shù)就是觀察者實例本身,一般沒用,因為實例通常我們已經(jīng)賦值給一個變量了,而且回調(diào)函數(shù)里的 this 也是那個實例。第一個參數(shù)是個包含有若干個 IntersectionObserverEntry 對象的數(shù)組,也就是和 takeRecords() 方法的返回值一樣。每個 IntersectionObserverEntry 對象都代表一次相交,它的屬性們就包含了那次相交的各種信息。entries 數(shù)組中 IntersectionObserverEntry 對象的排列順序是按照它所屬的目標元素當初被 observe() 的順序排列的。

time

相交發(fā)生時距離頁面打開時的毫秒數(shù)(有小數(shù)),也就是相交發(fā)生時 performance.now() 的返回值,比如 60000.560000000005,表示是在頁面打開后大概 1 分鐘發(fā)生的相交。在回調(diào)函數(shù)里用 performance.now() 減去這個值,就能算出回調(diào)函數(shù)被 requestIdleCallback 延遲了多少毫秒:

<script>
 let observer = new IntersectionObserver(([entry]) => {
 document.body.textContent += `相交發(fā)生在 ${performance.now() - entry.time} 毫秒前`
 })

 observer.observe(document.documentElement)
</script>

你可以不停刷新上面這個 demo,那個毫秒數(shù)最多 100 出頭,因為瀏覽器內(nèi)部設(shè)置的最大延遲就是 100。

target

相交發(fā)生時的目標元素,因為一個根元素可以觀察多個目標元素,所以這個 target 不一定是哪個元素。

rootBounds

一個對象值,表示發(fā)生相交時根元素可見區(qū)域的矩形信息,像這樣:

{
 "top": 0,
 "bottom": 600,
 "left": 0,
 "right": 1280,
 "width": 1280,
 "height": 600
}

boundingClientRect

發(fā)生相交時目標元素的矩形信息,等價于 target.getBoundingClientRect()。

intersectionRect

根元素和目標元素相交區(qū)域的矩形信息。

intersectionRatio

0 到 1 的數(shù)值,表示相交區(qū)域占目標元素區(qū)域的百分比,也就是 intersectionRect 的面積除以 boundingClientRect 的面積得到的值。

貼邊的情況是特例

上面已經(jīng)說過,IntersectionObserver API 的基本工作原理就是檢測相交率的變化。每個觀察者實例為所有的目標元素都維護著一個上次相交率(previousThreshold)的字段,在執(zhí)行 observe() 的時候會給 previousThreshold 賦初始值 0,然后每次檢測到新的相交率滿足(到達或跨過)了 thresholds 中某個指定的臨界值,且那個臨界值和當前的 previousThreshold 值不同,就會觸發(fā)回調(diào),并把滿足的那個新的臨界值賦值給 previousThreshold,依此反復,很簡單,對吧。

但是不知道你有沒有注意到,前面講過,當目標元素從距離根元素很遠到和根元素貼邊,這時也會觸發(fā)回調(diào)(假如 thresholds 里有 0),但這和工作原理相矛盾啊,離的很遠相交率是 0,就算貼邊,相交率還是 0,值并沒有變,不應該觸發(fā)回調(diào)啊。的確,這和基本工作原理矛盾,但這種情況是特例,目標元素從根元素外部很遠的地方移動到和根元素貼邊,也會當做是滿足了臨界值 0,即便 0 等于 0。

還有一個反過來的特例,就是目標元素從根元素內(nèi)部的某個地方(相交率已經(jīng)是 1)移動到和根元素貼邊(還是 1),也會觸發(fā)回調(diào)(假如 thresholds 里有 1)。

目標元素寬度或高度為 0 的情況也是特例

很多時候我們的目標元素是個空的 img 標簽或者是一個空的 div 容器,如果沒有設(shè)置 CSS,這些元素的寬和高都是 0px,那渲染出的矩形面積就是 0px2,那算相交率的時候就會遇到除以 0 這種在數(shù)學上是非法操作的問題,即便在 JavaScript 里除以 0 并不會拋異常還是會得到 Infinity,但相交率一直是 Infinity 也就意味著回調(diào)永遠不會觸發(fā),所以這種情況必須特殊對待。

特殊對待的方式就是:0 面積的目標元素的相交率要么是 0 要么是 1。無論是貼邊還是移動到根元素內(nèi)部,相交率都是 1,其它情況都是 0。1 到 0 會觸發(fā)回調(diào),0 到 1也會觸發(fā)回調(diào),就這兩種情況:

由于這個特性,所以為 0 面積的目標元素設(shè)置臨界值是沒有意義的,設(shè)置什么值、設(shè)置幾個,都是一個效果。

但是注意,相交信息里的 intersectionRatio 屬性永遠是 0,很燒腦,我知道:

<div id="target"></div>

<script>
 let observer = new IntersectionObserver(([entry]) => {
 alert(entry.intersectionRatio)
 })

 observer.observe(target)
</script>

observe() 之前就已經(jīng)相交了的情況是特例嗎?

不知道你們有沒有這個疑問,反正我有過。observe() 一個已經(jīng)和根元素相交的目標元素之后,再也不滾動頁面,意味著之后相交率再也不會變化,回調(diào)不應該發(fā)生,但還是發(fā)生了。這是因為:在執(zhí)行 observe() 的時候,瀏覽器會將 previousThreshold 初始化成 0,而不是初始化成當前真正的相交率,然后在下次相交檢測的時候就檢測到相交率變化了,所以這種情況不是特殊處理。

瀏覽器何時進行相交檢測,多久檢測一次?

我們常見的顯示器都是 60hz 的,就意味著瀏覽器每秒需要繪制 60 次(60fps),大概每 16.667ms 繪制一次。如果你使用 200hz 的顯示器,那么瀏覽器每 5ms 就要繪制一次。我們把 16.667ms 和 5ms 這種每次繪制間隔的時間段,稱之為 frame(幀,和 html 里的 frame 不是一個東西)。瀏覽器的渲染工作都是以這個幀為單位的,下圖是 Chrome 中每幀里瀏覽器要干的事情(我在原圖的基礎(chǔ)上加了 Intersection Observations 階段):

Intersection Observations In A Frame

可以看到,相交檢測(Intersection Observations)發(fā)生在 Paint 之后 Composite 之前,多久檢測一次是根據(jù)顯示設(shè)備的刷新率而定的。但可以肯定的是,每次繪制不同的畫面之前,都會進行相交檢測,不會有漏網(wǎng)之魚。

一次性到達或跨過的多個臨界值中選一個最近的

如果一個觀察者實例設(shè)置了 11 個臨界值:[0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1],那么當目標元素和根元素從完全不相交狀態(tài)滾動到相交率為 1 這一段時間里,回調(diào)函數(shù)會觸發(fā)幾次?答案是:不確定。要看滾動速度,如果滾動速度足夠慢,每次相交率到達下一個臨界值的時間點都發(fā)生在了不同的幀里(瀏覽器至少繪制了 11 次),那么就會有 11 次相交被檢測到,回調(diào)函數(shù)就會被執(zhí)行 11 次;如果滾動速度足夠快,從不相交到完全相交是發(fā)生在同一個幀里的,瀏覽器只繪制了一次,瀏覽器雖然知道這一次滾動操作就滿足了 11 個指定的臨界值(從不相交到 0,從 0 到 0.1,從 0.1 到 0.2 ··· ),但它只會考慮最近的那個臨界值,那就是 1,回調(diào)函數(shù)只觸發(fā)一次:

<div id="info">相交次數(shù):
 <span id="times">0</span>
 <button onclick="document.scrollingElement.scrollTop = 10000">一下滾動到最低部</button>
</div>
<div id="target"></div>

<style>
 #info {
 position: fixed;
 }

 #target {
 position: absolute;
 top: 200%;
 width: 100px;
 height: 100px;
 background: red;
 margin-bottom: 100px;
 }
</style>

<script>
 let observer = new IntersectionObserver(() => {
 times.textContent = +times.textContent + 1
 }, {
 threshold: [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1] // 11 個臨界值
 })

 observer.observe(target)
</script>

離開視口的時候也一個道理,假如根元素和目標元素的相交率先從完全相交變成了 0.45,然后又從 0.45 變成了完全不相交,那么回調(diào)函數(shù)只會觸發(fā)兩次。

如何判斷當前是否相交?

我上面有幾個 demo 都用了幾行看起來挺麻煩的代碼來判斷目標元素是不是在視口內(nèi):

if (!target.isIntersecting) {
 // 相交
 target.isIntersecting = true
} else {
 // 不想交
 target.isIntersecting = false
}

為什么?難道用 entry.intersectionRatio > 0 判斷不可以嗎:

<div id="info">不可見,請非常慢的向下滾動</div>
<div id="target"></div>

<style>
 #info {
 position: fixed;
 }

 #target {
 position: absolute;
 top: 200%;
 width: 100px;
 height: 100px;
 background: red;
 }
</style>

<script>
 let observer = new IntersectionObserver(([entry]) => {
 if (entry.intersectionRatio > 0) {
 // 快速滾動會執(zhí)行到這里
 info.textContent = "可見了"
 } else {
 // 慢速滾動會執(zhí)行到這里
 info.textContent = "不可見,請非常慢的向下滾動"
 }
 })

 observer.observe(target)
</script>

粗略一看,貌似可行,但你別忘了上面講的貼邊的情況,如果你滾動頁面速度很慢,當目標元素的頂部和視口底部剛好挨上時,瀏覽器檢測到相交了,回調(diào)函數(shù)觸發(fā)了,但這時 entry.intersectionRatio 等于 0,會進入 else 分支,繼續(xù)向下滾,回調(diào)函數(shù)再不會觸發(fā)了,提示文字一直停留在不可見狀態(tài);但如果你滾動速度很快,當瀏覽器檢測到相交時,已經(jīng)越過了 0 那個臨界值,存在了實際的相交面積,entry.intersectionRatio > 0 也就為 true 了。所以這樣寫會導致代碼執(zhí)行不穩(wěn)定,不可行。

除了通過在元素身上添加新屬性來記錄上次回調(diào)觸發(fā)時是進還是出外,我還想到另外一個辦法,那就是給 threshold 選項設(shè)置一個很小的接近 0 的臨界值,比如 0.000001,然后再用 entry.intersectionRatio > 0 判斷,這樣就不會受貼邊的情況影響了,也就不會受滾動速度影響了:

<div id="info">不可見,以任意速度向下滾動</div>
<div id="target"></div>

<style>
 #info {
 position: fixed;
 }

 #target {
 position: absolute;
 top: 200%;
 width: 100px;
 height: 100px;
 background: red;
 }
</style>

<script>
 let observer = new IntersectionObserver(([entry]) => {
 if (entry.intersectionRatio > 0) {
 info.textContent = "可見了"
 } else {
 info.textContent = "不可見,以任意速度向下滾動"
 }
 }, {
 threshold: [0.000001]
 })

 observer.observe(target)
</script>

目標元素不是根元素的后代元素的話會怎樣?

如果在執(zhí)行 observe() 時,目標元素不是根元素的后代元素,瀏覽器也并不會報錯,Chrome 從 53 開始會對這種用法發(fā)出警告(是我提議的),從而提醒開發(fā)者這種用法有可能是不對的。為什么不更嚴格點,直接報錯?因為元素的層級關(guān)系是可以變化的,可能有人會寫出這樣的代碼:

<div id="root"></div>
<div id="target"></div>

<style>
 #target {
 width: 100px;
 height: 100px;
 background: red;
 }
</style>

<script>
 let observer = new IntersectionObserver(() => alert("看見我了"), {root: root})
 observer.observe(target) // target 此時并不是 root 的后代元素,Chrome 控制臺會發(fā)出警告:target element is not a descendant of root.
 root.appendChild(target) // 現(xiàn)在是了,觸發(fā)回調(diào)
</script>

又或者被 observe 的元素此時還未添加到 DOM 樹里:

<div id="root"></div>

<style>
 #target {
 width: 100px;
 height: 100px;
 background: red;
 }
</style>

<script>
 let observer = new IntersectionObserver(() => alert("看見我了"), {root: root})
 let target = document.createElement("div") // 還不在 DOM 樹里
 observer.observe(target) // target 此時并不是 root 的后代元素,Chrome 控制臺會發(fā)出警告:target element is not a descendant of root.
 root.appendChild(target) // 現(xiàn)在是了,觸發(fā)回調(diào)
</script>

也就是說,只要在相交發(fā)生時,目標元素是根元素的后代元素,就可以了,執(zhí)行 observe() 的時候可以不是。

是后代元素還不夠,根元素必須是目標元素的祖先包含塊

要求目標元素是根元素的后代元素只是從 DOM 結(jié)構(gòu)上說的,一個較容易理解的限制,另外一個不那么容易理解的限制是從 CSS 上面說的,那就是:根元素矩形必須是目標元素矩形的祖先包含塊(包含塊也是鏈式的,就像原型鏈)。比如下面這個 demo 所演示的,兩個做隨機移動的元素 a 和 b,a 是 b 的父元素,但它倆的 position 都是 fixed,導致 a 不是 b 的包含塊,所以這是個無效的觀察操作,嘗試把 fixed 改成 relative 就發(fā)現(xiàn)回調(diào)觸發(fā)了:

<div id="a">
 <div id="b"></div>
</div>
<div id="info">0%</div>

<style>
 #a, #b {
 position: fixed; /* 嘗試改成 relative */
 width: 200px;
 height: 200px;
 opacity: 0.8;
 }

 #a {
 background: red
 }

 #b {
 background: blue
 }

 #info {
 width: 200px;
 margin: 0 auto;
 }

 #info::before {
 content: "Intersection Ratio: ";
 }
</style>

<script>
 let animate = (element, oldCoordinate = {x: 0, y: 0}) => {
 let newCoordinate = {
 x: Math.random() * (innerWidth - element.clientWidth),
 y: Math.random() * (innerHeight - element.clientHeight)
 }
 let keyframes = [oldCoordinate, newCoordinate].map(coordinateToLeftTop)
 let duration = calcDuration(oldCoordinate, newCoordinate)

 element.animate(keyframes, duration).onfinish = () => animate(element, newCoordinate)
 }

 let coordinateToLeftTop = coordinate => ({
 left: coordinate.x + "px",
 top: coordinate.y + "px"
 })

 let calcDuration = (oldCoordinate, newCoordinate) => {
 // 移動速度為 0.3 px/ms
 return Math.hypot(oldCoordinate.x - newCoordinate.x, oldCoordinate.y - newCoordinate.y) / 0.3
 }

 animate(a)
 animate(b)
</script>


<script>
 let thresholds = Array.from({
 length: 200
 }, (k, v) => v / 200) // 200 個臨界值對應 200px

 new IntersectionObserver(([entry]) => {
 info.textContent = (entry.intersectionRatio * 100).toFixed(2) + "%"
 }, {
 root: a,
 threshold: thresholds
 }).observe(b)
</script>

從 DOM 樹中刪除目標元素會怎么樣?

假設(shè)現(xiàn)在根元素和目標元素已經(jīng)是相交狀態(tài),這時假如把目標元素甚至是根元素從 DOM 樹中刪除,或者通過 DOM 操作讓目標元素不在是根元素的后代元素,再或者通過改變 CSS 屬性導致根元素不再是目標元素的包含塊,又或者通過 display:none 隱藏某個元素,這些操作都會讓兩者的相交率突然變成 0,回調(diào)函數(shù)就有可能被觸發(fā):

<div id="info"> 刪除目標元素也會觸發(fā)回調(diào)
 <button onclick="document.body.removeChild(target)">刪除 target</button>
</div>
<div id="target"></div>


<style>
 #info {
 position: fixed;
 }
 
 #target {
 position: absolute;
 top: 100px;
 width: 100px;
 height: 100px;
 background: red;
 }
</style>

<script>
 let observer = new IntersectionObserver(() => {
 if (!document.getElementById("target")) {
 info.textContent = "target 被刪除了"
 }
 })

 observer.observe(target)
</script>

關(guān)于 iframe

在 IntersectionObserver API 之前,你無法在一個跨域的 iframe 頁面里判斷這個 iframe 頁面或者頁面里的某個元素是否出現(xiàn)在了頂層窗口的視口里,這也是為什么要發(fā)明 IntersectionObserver API 的一個很重要的原因。請看下圖演示:

無論怎么動,無論多少層 iframe, IntersectionObserver 都能精確的判斷出目標元素是否出現(xiàn)在了頂層窗口的視口里,無論跨域不跨域。

前面講過根元素為 null 表示實際的根元素是當前窗口的視口,現(xiàn)在更明確點,應該是最頂層窗口的視口。

如果當前頁面是個 iframe 頁面,且和頂層頁面跨域,在根元素為 null 的前提下觸發(fā)回調(diào)后,你拿到的 IntersectionObserverEntry 對象的 rootBounds 屬性會是 null;即便兩個頁面沒有跨域,那么 rootBounds 屬性所拿到的矩形的坐標系統(tǒng)和 boundingClientRect 以及 intersectionRect 這兩個矩形也是不一樣的,前者坐標系統(tǒng)的原點是頂層窗口的左上角,后兩者是當前 iframe 窗口左上角。

鑒于互聯(lián)網(wǎng)上的廣告 90% 都是跨域的 iframe,我想 IntersectionObserver API 能夠大大簡化這些廣告的延遲加載和真實曝光量統(tǒng)計的實現(xiàn)。

根元素不能是其它 frame 下的元素

如果沒有跨域的話,根元素可以是上層 frame 中的某個祖先元素嗎?比如像下面這樣:

<div id="root">
 <iframe id="iframe"></iframe>
</div>

<script>
 let iframeHTML = `
 <div id="target"></div>

 <style>
 #target {
 width: 100px;
 height: 100px;
 background: red;
 }
 </style>

 <script>
 let observer = new IntersectionObserver(() => {
 alert("intersecting")
 }, {
 root: top.root
 })

 observer.observe(target)
 <\/script>`

 iframe.src = URL.createObjectURL(new Blob([iframeHTML], {"type": "text/html"}))
</script>

我不清楚上面這個 demo 中 root 算不算 target 的祖先包含塊,但規(guī)范明確規(guī)定了這種觀察操作無效,根元素不能是來自別的 frame??偨Y(jié)一下就是:根元素要么是 null,要么是同 frame 里的某個祖先包含塊元素。

真的只是判斷兩個元素相交嗎?

實際情況永遠沒表面看起來那么簡單,瀏覽器真的只是判斷兩個矩形相交嗎?看下面的代碼:

<div id="parent">
 <div id="target"></div>
</div>

<style>
 #parent {
 width: 20px;
 height: 20px;
 background: red;
 overflow: hidden;
 }

 #target {
 width: 100px;
 height: 100px;
 background: blue;
 }
</style>

<script>
 let observer = new IntersectionObserver(([entry]) => {
 alert(`相交矩形為: ${entry.intersectionRect.width} x ${entry.intersectionRect.width}`)
 })

 observer.observe(target)
</script>

這個 demo 里根元素為當前視口,目標元素是個 100x100 的矩形,如果真的是判斷兩個矩形的交集那么簡單,那這個相交矩形就應該是 100 x 100,但彈出來的相交矩形是 20 x 20。因為其實在相交檢測之前,有個裁減目標元素矩形的步驟,裁減完才去和根元素判斷相交,裁減的基本思想就是,把目標元素被“目標元素和根元素之間存在的那些元素”遮擋的部分裁掉,具體裁減步驟是這樣的(用 rect 代表最終的目標元素矩形):

1、讓 rect 為目標元素矩形
2、讓 current 為目標元素的父元素
3、如果 current 不是根元素,則進行下面的循環(huán):
如果 current 的 overflow 不是 visible(是 scroll 或 hidden 或 auto) 或者 current 是個 iframe 元素(iframe 天生自帶 overflow: auto),則:
讓 rect 等于 rect 和 current 的矩形(要排除滾動條區(qū)域)的交集
讓 current 為 current 的父元素(iframe 里的 html 元素的父元素就是父頁面里的 iframe 元素)
也就是說,實際上是順著目標元素的 DOM 樹一直向上循環(huán)求交集的過程。再看上面的 demo,目標元素矩形一開始是 100x100,然后和它的父元素相交成了 20x20,然后 body 元素和 html 元素沒有設(shè)置 overflow,所以最終和視口做交集的是 20x20 的矩形。

關(guān)于雙指縮放

移動端設(shè)備和 OS X 系統(tǒng)上面,允許用戶使用兩根手指放大頁面中的某一部分:

如果頁面某一部分被放大了,那同時也就意味著頁面邊緣上某些區(qū)域顯示在了視口的外面:

這些情況下 IntersectionObserver API 都不會做專門處理,無論是根元素還是目標元素,它們的矩形都是縮放前的真實尺寸(就像 getBoundingClientRect() 方法所表現(xiàn)的一樣),而且即便相交真的發(fā)生在了那些因縮放導致用戶眼睛看不到的區(qū)域內(nèi),回調(diào)函數(shù)也照樣觸發(fā)。如果你用的 Mac 系統(tǒng),你現(xiàn)在就可以測試一下上面的任意一個 demo。

關(guān)于垃圾回收

一個觀察者實例無論對根元素還是目標元素,都是弱引用的,就像 WeakMap 對自己的 key 是弱引用一樣。如果目標元素被垃圾回收了,關(guān)系不大,瀏覽器就不會再檢測它了;如果是根元素被垃圾回收了,那就有點問題了,根元素沒了,但觀察者實例還在,如果這時使用哪個觀察者實例會怎樣:

<div id="root"></div>
<div id="target"></div>

<script>
 let observer = new IntersectionObserver(() => {}, {root: root}) // root 元素一共有兩個引用,一個是 DOM 樹里的引用,一個是全局變量 root 的引用
 document.body.removeChild(root) // 從 DOM 樹里移除
 root = null // 全局變量置空
 setTimeout(() => {
 gc() // 手動 gc,需要在啟動 Chrome 時傳入 --js-flags='--expose-gc' 選項
 console.log(observer.root) // null,觀察者實例的根元素已經(jīng)被垃圾回收了
 observer.observe(target) // Uncaught InvalidStateError: observe() called on an IntersectionObserver with an invalid root,執(zhí)行 observer 的任意方法都會報錯。
 })
</script>

也就是說,那個觀察者實例也相當于死了。這個報錯是從 Chrome 53 開始的(我提議的),51 和 52 上只會靜默失敗。

后臺標簽頁

由于 Chrome 不會渲染后臺標簽頁,所以也就不會檢測相交了,當你切換到前后才會繼續(xù)。你可以通過 Command/Ctrl + 左鍵打開上面任意的 demo 試試。

吐槽命名

threshold 和 thresholds

構(gòu)造函數(shù)的參數(shù)里叫 threshold,實例的屬性里叫 thresholds。道理我都懂,前者既能是一個單數(shù)形式的數(shù)字,也能是一個復數(shù)形式的數(shù)組,所以用了單數(shù)形式,而后者序列化出來只能是個數(shù)組,所以就用了復數(shù)了。但是統(tǒng)一更重要吧,我覺的都用復數(shù)形式?jīng)]什么問題,一開始研究這個 API 的時候我嘗試傳了 {thresholds: [1]},試了半天才發(fā)現(xiàn)多了個 s,坑死了。

disconnect

什么?disconnect?什么意思?connect 什么了?我只知道 observe 和 unobserve,你他么的叫 unobserveAll 會死啊。這個命名很容易讓人不明覺厲,結(jié)果是個很簡單的東西。叫這個其實是為了和 MutationObserver 以及 PerformanceObserver 統(tǒng)一。

rootBounds & boundingClientRect & intersectionRect

這三者都是返回一個矩形信息的,本是同類,但是名字沒有一點規(guī)律,讓人無法記憶。我建議叫 rootRect & targetRect & intersectionRect,一遍就記住了,真不知道寫規(guī)范的人怎么想的。

Polyfil

寫規(guī)范的人會在 Github 倉庫上維護一個 polyfill,目前還未完成。但 polyfill 顯然無法支持 iframe 內(nèi)元素的檢測,不少細節(jié)也無法模擬。

其它瀏覽器實現(xiàn)進度

Firefox:https://bugzilla.mozilla.org/show_bug.cgi?id=1243846

Safari:https://bugs.webkit.org/show_bug.cgi?id=159475

Edge:https://developer.microsoft.com/en-us/microsoft-edge/platform/status/intersectionobserver

總結(jié)

雖然目前該 API 的規(guī)范已經(jīng)有一年歷史了,但仍非常不完善,大量的細節(jié)都沒有規(guī)定;Chrome 的實現(xiàn)也有半年了,但還是有不少 bug(大多是疑似 bug,畢竟規(guī)范不完善)。因此,本文中有些細節(jié)我故意略過,比如目標元素大于根元素,甚至根元素面積為 0,支不支持 svg 這些,因為我也不知道什么是正確的表現(xiàn)。

2016-8-2 追記:今天被同事問了個真實需求,“統(tǒng)計淘寶搜索頁面在頁面打開兩秒后展現(xiàn)面積超過 50% 的寶貝”,我立刻想到了用 IntersectionObserver:

setTimeout(() => {
 let observer = new IntersectionObserver(entries => {
 entries.forEach(entry => {
 console.log(entry.target) // 拿到了想要的寶貝元素
 })
 observer.disconnect() // 統(tǒng)計到就不在需要繼續(xù)觀察了
 }, {
 threshold: 0.5 // 只要展現(xiàn)面積達到 50% 的寶貝元素 
 })

 // 觀察所有的寶貝元素
 Array.from(document.querySelectorAll("#mainsrp-itemlist .item")).forEach(item => observer.observe(item))
}, 2000)

不需要你進行任何數(shù)學計算,真是簡單到爆,當然,因為兼容性問題,這個代碼不能被采用。

相關(guān)文章

  • 原生JS實現(xiàn)pc端輪播圖效果

    原生JS實現(xiàn)pc端輪播圖效果

    這篇文章主要為大家詳細介紹了原生JS實現(xiàn)pc端輪播圖效果,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2020-12-12
  • ES6 系列之 Generator 的自動執(zhí)行的方法示例

    ES6 系列之 Generator 的自動執(zhí)行的方法示例

    這篇文章主要介紹了ES6 系列之 Generator 的自動執(zhí)行的方法示例,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2018-10-10
  • 微信小程序傳值常用的4種方式

    微信小程序傳值常用的4種方式

    微信小程序開發(fā)中的大部分知識點和前端開發(fā)是一模一樣的,這篇文章主要給大家介紹了關(guān)于微信小程序傳值常用的4種方式,文中通過實例代碼介紹的非常詳細,需要的朋友可以參考下
    2023-05-05
  • JS實現(xiàn)字符串中去除指定子字符串方法分析

    JS實現(xiàn)字符串中去除指定子字符串方法分析

    這篇文章主要介紹了JS實現(xiàn)字符串中去除指定子字符串方法,結(jié)合實例形式分析了javascript使用字符串替換與分割、聚合兩種子字符串去除相關(guān)操作技巧,需要的朋友可以參考下
    2018-05-05
  • JS+CSS實現(xiàn)的藍色table選項卡效果

    JS+CSS實現(xiàn)的藍色table選項卡效果

    這篇文章主要介紹了JS+CSS實現(xiàn)的藍色table選項卡效果,通過鼠標事件調(diào)用自定義函數(shù)實現(xiàn)頁面元素樣式的遍歷與動態(tài)切換效果,具有一定參考借鑒價值,需要的朋友可以參考下
    2015-10-10
  • 最新評論