IntersectionObserver?拿超商的歡迎光臨來說明
一開始知道有「IntersectionObserver」這個 Web API,是在一篇講圖片 lazy load 的文章上看到的。
以前我們要做圖片的延遲載入(lazy load),會用 onscroll
來監聽圖片那個標籤的 offset().Top
是不是怎麼樣又怎麼樣的計算後,確定圖片出現在視窗上了,才把圖片的路徑塞進 src 裡,以前 August 就寫過這樣:
$('selector').offset().top - $(window).scrollTop() <= $(window).height()
這個的缺點就是 onscroll,瀏覽器會在每一次的捲動都執行監聽的動作。
而 IntersectionObserver
這個 API,很純悴的就是指定的目標出現在觀察器(window)中時,就傳一個 true 來告知。
onscroll 跟 IntersectionObserver 有什麼不一樣呢?我們來用便利商店來舉例。
假設今天你是員工,老闆要求只要有客人進來,你都要喊「歡迎光臨」。
onscroll 指的就是,從此後你就一直盯著門口,確認路人A進來時就喊歡迎光臨。這樣沒什麼不對,你確實達成目標了,但你也要花心力一直盯著門口看,做其它事情也心驚膽跳的怕有人來。
那 IntersectionObserver 是什麼呢?就是自動門,有人進來時它就發出「叮咚(true)」告訴你有人進來了。那是不是你就可以安心的做其它事,自動門發出聲音時再喊就好?
換成網頁的角度來看,「你」就是瀏覽器,「客人」就是要延遲載入的 img,那只要聽到 IntersectionObserver 發出叮咚了,再來執行 img src 放上正確路徑這個函式就行。
本篇會筆記的分成上、下 2 篇。
上篇,就是本篇,是 August 用自己的方式理解 IntersectionObserver 的使用方式,以及相關參數說明。
下篇,預計提供幾個實用的範例,像是 lazy load、無限滾動、側邊欄固定等。
另外為了不讓 IntersectionObserver 這麼長的名稱干擾閱讀,以下簡稱「IO」。
IO 使用 3 步驟
使用 IO 的步驟很簡單,就 3 步,如下:
- 建立觀察器(observer),觀察器要有個鏡頭來觀察,這個鏡頭就是 root,root 如果沒有指定,預設就是指 window
- 指定觀察器要觀察的獵物(entry)
- 當獵物 進入(true) / 離開(false) 觀察器視窗的範圍,觀察器就執行 callback
在寫這篇前看了幾篇教學文(推薦的三篇會附在文未),因為寫的是高手,所以沒有像 August 一樣需要一個完整的使用步驟來理解及記憶,痛定思痛,不希望看到這篇的你對 IO 一頭霧水,就先整理出這三個步驟,以下開始筆記這三個步驟的程式碼。
1 建立觀察器(observer)
建立觀察器很簡單,就一行:
var observer = new IntersectionObserver(callback, [option]);
callback 就是當獵物(entry)進入到觀察器的鏡頭(root)內時,要做什麼事的 function。
option 是選填,不填就是預設值,這一段 August 是直接用一個 console.log 來看會 log 出什麼來理解的,看到 console.log 出 option 的東西後就懂了:
option 就是調整觀察器的鏡頭用的。
option 總共有以下幾個值可以填:
{ root: null, rootMargin: "0px 0px 0px 0px", threshold: [0] }
root
這邊想像成觀察器的鏡頭比較好理解。當目標進入到鏡頭內就 callback,離開了鏡頭也 callback。root 可以設一個 element,比方寫:
root: document.getElementById('container');
root 的預設是 null,root 為 null 時就代表鏡頭就是你的視窗,就是螢幕正在看的區域。
rootMargin
4 個值分別代表上、右、下、左,這個是放大或縮小鏡頭的邊界用的。比方你的螢幕只有 13 寸,但你想讓鏡頭的範圍拉大到 15 寸,這樣就可以讓目標離不開你的視線,那就填 rootMargin 來拉大。
rootMargin 的預設值是 "0px 0px 0px 0px"
。
2019.10.28 補充:
rootMargin,實際寫 Demo 頁時,發現使用起來完全不是理解的這麼回事。
比方想把鏡頭往下移 2000px,寫了2000px 0px 0px 0px
後,卻還是一樣目標一出現到視窗底部,就判斷entry.isIntersecting === true
了。
Google 了一下,也有其他人有遇到這問題:IntersectionObserver rootMargin’s positive and negative values are not working,目前還沒看到有人回答。
所以關於 rootMargin 這點,如果有大大知道原因,敬請於回覆區中回覆。
2020.07.08 補充:
後來實測,rootMargin 要想像成是移動整個鏡頭。比方 top 的值寫了 -100px 往上移,那在 bottom 的部份要相對應的寫成 100px,rootMargin 就可以寫這樣:-100px 0px 100px 0px
。實測這樣子寫是成功的。
threshold
指獵物本身出現了多少部份在你的鏡頭裡,而出現的部份到了指定的百分比後,都會執行 callback。這個直接看圖比較好懂:
綠色的方塊出現在視窗中的範圍不同,上面那一排紅底的字就會顯示不同文字。
threshold 的預設值是 [0],就是緣色方塊一接觸到視窗的邊就 callback,像上圖就是設成:[0, 0.25, 0.5, 0.75, 1]
,代表綠色方塊出現了 0%、25%、50%、75%、100% 這 5 個範圍後,都要執行一次 callback。
再看一次建立觀察器的 code:
var observer = new IntersectionObserver(callback, [option]);
option 上面介紹完了,callback 會寫在第 3 步,本段要再說明一小段,就是當 observer 這個變數被命出來後,代表觀察器建好了,它本身可以做 3 件事:
- 開始觀察某個獵物:
observer.observe(el)
- 取消觀察某個獵物:
observer.unobserve(el)
- 自爆,關掉這個觀察器:
observer.disconnect()
2 指定觀察器要觀察的獵物(entry)
第一步把觀察器建出來,接著要開始找獵物來觀察。比方我們想觀察 <img id="img">
,就可以寫:
function callback(entry) {} var observer = new IntersectionObserver(callback); var img = document.getElementById('img'); observer.observe(img);
那當然,也可以直接用個迴圈觀察所有圖片:
function callback(entry) {} var observer = new IntersectionObserver(callback); var all_img = document.querySelectorAll('img'); Array.prototype.forEach.call(all_img, function(img) { observer.observe(img); });
3 當獵物 進入(true) / 離開(false) 觀察器視窗的範圍,觀察器就執行 callback
上面第 2 步的 code 可以看到, function callback(entry)
這邊,IO 的 callback 會帶一個參數進來,常看到命名為 entry 或 entries,之所以會用複數是因為這個參數會是一個陣列。
像第 2 步的 code,有觀察 1 張圖片的,或是觀察多張圖片的,callback 帶進來的都會是陣列,不管觀察的獵物是 1 個或多個。
我們來 console.log(entry)
看一下 entry 這個陣列有什麼:
[ { // ReadOnly:目標元素的矩形區域的信息 boundingClientRect: { /* ... */ }, // 獵物的可見比例 intersectionRatio: 1, // ReadOnly:獵物與root的交叉區域 intersectionRect: { /* ... */ }, // 是否出現在鏡頭(root))中 isIntersecting: true, // ReadOnly:鏡頭(root)的資訊 rootBounds: { /* ... */ }, // 獵物本身 target: 獵物的DOM節點 } ]
這邊講 3 個比較常會用到的:intersectionRatio、isIntersecting、target。
intersectionRatio
獵物的可見比例,這跟 IO 的 threshold 很像,都是在看獵物出現在視窗中多少部份。不一樣的是 IO 是指定全部獵物,而 intersectionRect 是指能讀,不能改變,可以讀出每個獵物出現了多少部份,直接看圖比較好懂:
可以看到,鏡頭裡上面的出現了 75%,中間的出現了 100%,最下面的出現了 12%。
target
target 就是這個獵物在 DOM 上的節點,所以可以直接用 entry[0].target
來抓到它。
isIntersecting
這個就是最重要的,就是超商那個例子的自動門「叮咚」聲。
當獵物進入到鏡頭後,isIntersecting 會是 true,不在鏡頭內就是 false。
所以再拿圖片 lazy load 來當示範,就可以這樣寫:
function callback(entry) { if(entry[0].isIntersecting) { entry[0].target.src = entry[0].target.dataset.src; observer.unobserve(entry[0].target); } } var observer = new IntersectionObserver(callback); var img = document.getElementById('img'); observer.observe(img);
在 entry[0].isIntersecting
為 true
時就執行替換 img src 的動作,並且取消對這個獵物的觀察,這樣替換 src 的動作就只會執行這一次。
原始碼
IO 的 3 個步驟筆記完了,IO 也會使用了,這邊附上 August 整理過的程式碼,忘記的話以後可以直接看原始碼來找:
參考資源
本篇筆記主要看了 3 篇的教學文跟文件:
- IntersectionObserver’s Coming into View
- IntersectionObserver API 使用教程:算是上一個的中文版吧
- Intersection Observer
本篇主要是 IO 的基本使用,下一篇會開始實作幾個範例。
IntersectionObserver:下篇 – 實際應用 lazyload、進場效果、無限捲動
rootMargin 把鏡頭往下移的部分
寫法應該是
我自己測試可是以達成鏡頭下移的效果的
請再測試看看是否有效
另外謝謝你整理的資訊
非常清楚明瞭
之前有測過,但沒用,有測到過一次有用的,是要寫像這樣:
相反面要跟著移。
我把 rootMargin 理解成把可視區域縮小。
比如上方 fixed 的 navbar 有 90px ,我就設 {rootMargin:’-90px 0px 0px 0px’}
理解成移動整個鏡頭會比較好,top為-90px時,bottom的值相對的也要寫成90px。
這是後來的實測,這樣子寫就可以了。