一文詳解前端進(jìn)階之IntersectionObserver
背景介紹
作為一款產(chǎn)品,往往希望能得到用戶的反饋,從而通過(guò)對(duì)用戶行為的分析進(jìn)行功能、交互等方面的改進(jìn)。然而直接的一對(duì)一的用戶交流是低效且困難的,因此最普遍的做法便是通過(guò)數(shù)據(jù)埋點(diǎn)來(lái)反推用戶的行為。那么數(shù)據(jù)埋點(diǎn)中很重要的一環(huán)便是:曝光。
所謂曝光,便是頁(yè)面被展示的時(shí)候進(jìn)行打點(diǎn)。舉個(gè)簡(jiǎn)單的例子:用戶進(jìn)入分類頁(yè)面,商品以行為單位從上而下進(jìn)行排列。當(dāng)用戶滾動(dòng)頁(yè)面時(shí),之前不在視窗范圍內(nèi)的商品就會(huì)出現(xiàn),此時(shí),這部分商品就算曝光了,需要進(jìn)行一次記錄。
那么為了實(shí)現(xiàn)上面功能,最普遍的做法有兩個(gè)。其一:監(jiān)聽(tīng)滾動(dòng)事件,然后計(jì)算某個(gè)商品與視窗的相對(duì)位置,從而判斷是否可見(jiàn)。其二:設(shè)置一個(gè)定時(shí)器,然后以固定的時(shí)間為間隔計(jì)算某個(gè)商品與視窗的相對(duì)位置。
上面兩種做法在某種程度上能夠?qū)崿F(xiàn)我們的目的,但是會(huì)有一些問(wèn)題,比如最明顯的:慢。因?yàn)橛?jì)算相對(duì)位置時(shí)會(huì)調(diào)用getBoundingClientRect(),這個(gè)api會(huì)導(dǎo)致瀏覽器進(jìn)行全頁(yè)面的重新布局,影響性能,特別是在頻繁進(jìn)行時(shí)。因此IntersectionObserver API進(jìn)入了我們的視野。
IntersectionObserver API介紹
關(guān)于IntersectionObserver API的官方文檔見(jiàn)此。兼容性如下圖所示:
簡(jiǎn)單的說(shuō)IntersectionObserver讓你知道什么時(shí)候observe的元素進(jìn)入或者存在在root區(qū)域里了。下面我們來(lái)看下這個(gè)API的具體內(nèi)容:
// 用構(gòu)造函數(shù)生成觀察者實(shí)例,回調(diào)函數(shù)是必須的,后面的配置對(duì)象是可選的 const observer = new IntersectionObserver(changes => { for (const change of changes) { console.log(change.time); // 相交發(fā)生時(shí)經(jīng)過(guò)的時(shí)間 console.log(change.rootBounds); // 表示發(fā)生相交時(shí)根元素可見(jiàn)區(qū)域的矩形信息,是一個(gè)對(duì)象值 console.log(change.boundingClientRect); // target.boundingClientRect()發(fā)生相交時(shí)目標(biāo)元素的矩形信息,也是個(gè)對(duì)象值 console.log(change.intersectionRect); // 根元素與目標(biāo)元素相交時(shí)的矩形信息 console.log(change.intersectionRatio); // 表示相交區(qū)域占目標(biāo)區(qū)域的百分比,是一個(gè)0到1的值 console.log(change.target); // 相交發(fā)生時(shí)的目標(biāo)元素 } }, { root: null, threshold: [0, 0.5, 1], rootMargin: "50px" }); // 實(shí)例屬性 observer.root observer.rootMargin observer.thresholds // 實(shí)例方法 observer.observe(target); // 觀察針對(duì)某個(gè)特定元素的相交事件 observer.unobserve(target); // 停止對(duì)某個(gè)特定元素的相交事件的觀察 observer.disconnect(); // 停止對(duì)所有目標(biāo)元素的閾值事件的觀察,簡(jiǎn)單的說(shuō)就是停用整個(gè)IntersectionObserver // 除了上面三個(gè)實(shí)例方法,還有一個(gè)takeRecords()的方法,之后會(huì)詳細(xì)介紹
IntersectionObserver API允許開(kāi)發(fā)人員了解目標(biāo)dom元素相對(duì)于intersection root的可見(jiàn)性。這個(gè)root可以通過(guò)實(shí)例屬性獲取。默認(rèn)情況下它是null,此時(shí)它不是真正意義上的元素,它指視窗范圍,因此只要視窗范圍內(nèi)的目標(biāo)元素滾入視窗時(shí),就會(huì)觸發(fā)回調(diào)函數(shù)(如果root元素不存在了,則執(zhí)行其任何的observe都會(huì)出錯(cuò))。
我們可以在配置對(duì)象中將root改為具體的元素,此時(shí)當(dāng)目標(biāo)元素出現(xiàn)在root元素中時(shí)會(huì)觸發(fā)回調(diào),注意,在這種情況下相交可能發(fā)生在視窗下面。具體代碼在下,感興趣的同學(xué)可以試一下:
<!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ù)打印出來(lái)的對(duì)象中有一個(gè)intersectionRatio值,這個(gè)值其實(shí)涉及到了整個(gè)API的核心功能:當(dāng)目標(biāo)元素和根元素相交的面積占目標(biāo)元素面積的百分比到達(dá)或跨過(guò)某些指定的臨界值時(shí)就會(huì)觸發(fā)回調(diào)函數(shù)。因此相對(duì)的在配置對(duì)象里有一個(gè)threshold來(lái)對(duì)這個(gè)百分比進(jìn)行配置,默認(rèn)情況下這個(gè)值是[0],注意里面的值不能在0-1之外,否則會(huì)報(bào)錯(cuò)。我們舉個(gè)例子如下:
let ele = new IntersectionObserver( (entries) => { console.log(entries); }, { threshold: [0, 0.5, 1.0] } ); ele.observe(target);
在上面這個(gè)例子中,我們?cè)O(shè)定了0,0.5,1.0這三個(gè)值,因此當(dāng)交叉區(qū)域跨越0,0.5,1.0時(shí)都會(huì)觸發(fā)回調(diào)函數(shù)。注意我這邊的用詞是跨越,而不是到達(dá)。因?yàn)闀?huì)存在以下兩種情況導(dǎo)致回調(diào)打印出來(lái)的intersectionRatio不為0,0.5和1.0。
一、瀏覽器對(duì)相交的檢測(cè)是有時(shí)間間隔的。瀏覽器的渲染工作都是以幀為單位的,而IntersectionObserver是發(fā)生在幀里面的。因此假如你設(shè)定了[0,0.1,0.2,0.3,0.4,0.5]這個(gè)threshold,但是你的滾動(dòng)過(guò)程特別快,導(dǎo)致所有的繪制在一幀里面結(jié)束了,此時(shí)回調(diào)只會(huì)挑最近的臨界值觸發(fā)一次。
二、 IntersectionObserver是異步的。在瀏覽器內(nèi)部,當(dāng)一個(gè)觀察者實(shí)例觀察到眾多的相交行為時(shí),它不會(huì)立即執(zhí)行。關(guān)于IntersectionObserver的草案里面寫明了其實(shí)現(xiàn)是基于requestIdleCallback()來(lái)異步的執(zhí)行我們的回調(diào)函數(shù)的,并且規(guī)定了最大的延遲時(shí)間是100ms。關(guān)于這部分涉及到前面第一段代碼里的一個(gè)實(shí)例方法takeRecords()。如果你很迫切的希望馬上知道是否有相交,你不希望等待可能的100ms,此時(shí)你就能調(diào)用takeRecords(),此后你能馬上獲得包含IntersectionObserverEntry 對(duì)象的數(shù)組,里面有相交信息,如果沒(méi)有任何相交行為發(fā)生,則返回一個(gè)空數(shù)組。但這個(gè)方法與正常的異步回調(diào)是互斥的,如果它先執(zhí)行了則正?;卣{(diào)里面就沒(méi)信息了,反之亦然。
除開(kāi)上面的問(wèn)題,如果目標(biāo)元素的面積為0會(huì)產(chǎn)生什么情況呢?因?yàn)榕c0計(jì)算相交率是沒(méi)有意義的,實(shí)際我們舉個(gè)例子:
<!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>
我們會(huì)看到,雖然我們?cè)O(shè)定了0.5這個(gè)閾值,但實(shí)際回調(diào)只會(huì)在0與1.0時(shí)觸發(fā)。這是一種特殊的處理方式。
這里需要強(qiáng)調(diào)一點(diǎn)的是,我們的目標(biāo)元素在Observe的時(shí)候可以不存在的(注意這里的不存在是指沒(méi)有插入dom結(jié)構(gòu),但是元素本身是需要存在的),只需要在相交發(fā)生時(shí)存在就行了,我們來(lái)舉個(gè)栗子:
<!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>
同理,如果目標(biāo)元素與根元素處于相交狀態(tài),但是在一段時(shí)間后目標(biāo)元素不存在了(比如remove,或者display:none)了,那么此時(shí)依然會(huì)觸發(fā)一次回調(diào)。但是如果本身就不處于相交狀態(tài),然后消失掉了,因?yàn)?->0沒(méi)有變化,所以不會(huì)觸發(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)上的很多小廣告都是通過(guò)iframe嵌入的,然而現(xiàn)有的情況下很難獲取iframe在頂層視窗內(nèi)的曝光,但是使用IntersectionObserver API我們卻可以做到這點(diǎn)。下面舉個(gè)例子:
<!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)時(shí)觸發(fā)回調(diào),而且threshold值同樣能夠起作用。這樣一來(lái),大大簡(jiǎn)化了此類情況下獲取曝光的難度。
延遲加載與無(wú)限滾動(dòng)
上面我們關(guān)于配置參數(shù)已經(jīng)提到了root和threshold,實(shí)際上還有一個(gè)值:rootMargin。這個(gè)值實(shí)際就是給根元素添加了一個(gè)假想的margin值。使用場(chǎng)景最普遍的是用于延遲加載。因?yàn)槿绻娴牡饶繕?biāo)元素與根元素相交的時(shí)候再進(jìn)行加載圖片等功能就已經(jīng)晚了,所以有一個(gè)rootMargin值,這樣等于根元素延伸開(kāi)去了,目標(biāo)元素只要與延伸部分相交就會(huì)觸發(fā)回調(diào),下面我們來(lái)繼續(xù)舉個(gè)例子:
<!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>
在上面的例子中,目標(biāo)元素并沒(méi)有出現(xiàn)在根元素的視窗里的時(shí)候就已經(jīng)觸發(fā)回調(diào)了。
整個(gè)API可以用來(lái)實(shí)現(xiàn)無(wú)限滾動(dòng)和延遲加載,下面就分別舉出兩個(gè)簡(jiǎn)單的例子來(lái)啟發(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>
無(wú)限滾動(dòng)的例子:
<!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>
通篇看下來(lái)大家是不是感覺(jué)這個(gè)API還是很好玩的,api已經(jīng)問(wèn)世很多年了,大部分瀏覽器都可以兼容,低版本瀏覽器可以通過(guò)Polyfill解決,規(guī)范制訂者在github上發(fā)布了Polyfill。
利弊介紹
優(yōu)點(diǎn)
- 性能比直接的監(jiān)聽(tīng)scroll事件或者設(shè)置timer都好
- 使用簡(jiǎn)單
- 利用它的功能組合可以實(shí)現(xiàn)很多其他效果,比如無(wú)限滾動(dòng)等
- 對(duì)iframe的支持好
缺點(diǎn)
- 它不是完美像素與無(wú)延遲的,畢竟根本上是異步的。因此不適合做滾動(dòng)動(dòng)畫
以上就是一文詳解前端進(jìn)階之IntersectionObserver的詳細(xì)內(nèi)容,更多關(guān)于前端IntersectionObserver的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
javascript延時(shí)重復(fù)執(zhí)行函數(shù) lLoopRun.js
javascript延時(shí)重復(fù)執(zhí)行函數(shù) lLoopRun.js...2007-06-06Raphael帶文本標(biāo)簽可拖動(dòng)的圖形實(shí)現(xiàn)代碼
Javascript和Raphael順便學(xué)習(xí)了一下,主要是為了實(shí)現(xiàn)一個(gè)可拖動(dòng)的矩形同時(shí)矩形上還得顯示標(biāo)簽,網(wǎng)上關(guān)于這方面的知識(shí)提的很是于是本人自不量力寫了一下,感興趣的你可不要錯(cuò)過(guò)了哈,希望可以幫助到你2013-02-02深入理解javascript動(dòng)態(tài)插入技術(shù)
這篇文章介紹了javascript動(dòng)態(tài)插入技術(shù),有需要的朋友可以參考一下2013-11-11關(guān)聯(lián)的Select,可以支持客戶端動(dòng)態(tài)更新
關(guān)聯(lián)的Select,可以支持客戶端動(dòng)態(tài)更新...2006-09-09淺析JS刷新框架中的其他頁(yè)面 && JS刷新窗口方法匯總
本篇文章是對(duì)JS刷新框架中的其他頁(yè)面以及JS刷新窗口的方法進(jìn)行了匯總介紹,需要的朋友可以參考下2013-07-07JavaScript數(shù)組常用方法實(shí)例講解總結(jié)
這篇文章主要介紹了JavaScript數(shù)組及常見(jiàn)方法,結(jié)合實(shí)例形式總結(jié)分析了JavaScript數(shù)組的基本獲取、添加、刪除、排序、翻轉(zhuǎn)等相關(guān)操作技巧,需要的朋友可以參考下2021-09-09js通過(guò)地址欄給action傳值(中文亂碼全是問(wèn)號(hào))
我從js代碼中通過(guò)地址欄傳值給了action的相應(yīng)變量,但是,如果變量值為中文的時(shí)候,在action中測(cè)試輸出則為問(wèn)號(hào)2013-05-05