比如說,你想跟蹤 DOM 樹里的一個元素,當它進入可見窗口時得到通知。 也許想實現即時延遲加載圖片功能,或者你需要知道用戶是否真的在看一個廣告 banner。 你可以通過綁定 scroll
事件或者用一個周期性的定時器,然后再回調函數中調用元素的 getBoundingClientRect()
獲取元素位置實現這個功能。 但是,這種實現方式性能極差,因為每次調用 getBoundingClientRect()
都會強制瀏覽器重新計算整個頁面的布局,可能給你的網站造成相當大的閃爍。 如果你的站點被加載到一個 iframe 里,而你想要知道用戶什么時候能看到某個元素,這幾乎是不可能的。 單原模型(Single Origin Model)和瀏覽器不會讓你獲取 iframe 里的任何數據。 這對於經常在 iframe 里加載的廣告頁面來說是一個很常見的問題。
IntersectionObserver 就是為此而生的,它讓檢測一個元素是否可見更加高效,而且已經在 Chrome 51 中實現。 IntersectionObserver
能讓你知道一個被觀測的元素什么時候進入或離開瀏覽器的可見窗口。
如何創建一個 IntersectionObserver
API 比較簡單,最好用一個例子說明:
var io = new IntersectionObserver( entries => { console.log(entries); }, { /* 使用默認參數。下面詳細說明 */ } ); // 開始觀測某個元素 io.observe(element); // 停止關注某個元素 // io.unobserve(element); // 禁用整個 IntersectionObserver // io.disconnect();
使用 IntersectionObserver
的默認屬性,當元素部分進入可見窗口或完全離開可見窗口時都會調用你的回調函數。
如果你需要觀測多個元素,你可以用——而且是推薦使用——同一個IntersectionObserver
實例調用多次 observe()
。
一個 entries 參數會被傳遞給你的回調函數,它是一個IntersectionObserverEntry 對象數組。 每個對象都包含更新過的交點數據針對你所觀測的元素之一。
[IntersectionObserverEntry] time: 3893.92 🔽rootBounds: ClientRect bottom: 920 height: 1024 left: 0 right: 1024 top: 0 width: 920 🔽boundingClientRect: ClientRect // ... 🔽intersectionRect: ClientRect // ... intersectionRatio: 0.54 🔽target: div#observee // ...
rootBounds 是在根元素上調用 getBoundingClientRect()
的結果,默認就是可見窗口。boundingClientRect 是在被觀測元素上調用 getBoundingClientRect
的結果。intersectionRect 是這兩個矩形的交界,它告訴你被觀測的哪塊區域是可見的。intersectionRatio 把兩個緊緊關聯起來,告訴你元素有多少可見。 有這些信息供你使用,你就能非常高效地實現一些像即時加載資源等功能。
IntersectionObserver 總是異步傳輸這些數據,你的回調函數代碼會在主線程運行。
另外,規范明確指出 IntersectionObserver 實現應該使用requestIdleCallback()。 這意味着調用你注冊的回調函數的優先級較低,瀏覽器在空閑時間才會這樣做。 這是一個有意識的設計決定。
滾動 div
我不是很喜歡在某個元素里滾動,但我不會再這里和你爭,IntersectionObserver 也不是。 當你不是在滾動整個可見窗口時,options 對象可帶一個 root 參數用於指定滾動的元素。 有一點很重要你需要牢記:root
需要是所有被觀測元素的直接或間接父級。
檢測所有東西!
不能這樣!作為一個開發者寫出這樣的代碼是不稱職的。 這種用法對用戶的 CPU 非常不友好。 考慮一個無線滾動的例子,在這種情況下,更為可取的方案是在 DOM 添加崗哨,觀測(並且復用)他們。 你應該在無線滾動區域的最后一個元素之后添加一個崗哨。 當那個崗哨進入可見窗口時,你就可以在回調函數里加載數據,創建后面的元素,添加到 DOM 里,並且隨之更新崗哨的位置。 如果你正確的復用了這些崗哨,就無需在調用 observe()
。 IntersectionObserver 扔可以繼續工作。
Moar Updates, Please
如同之前所說,當被觀測的元素部分進入可見窗口時會觸發回調函數一次,當它離開可見窗口時會觸發另一次。 這樣就回答了一個問題:元素 X 在不在可見窗口里。 但在某些場合,僅僅如此還不夠。
就就輪到 threshold 起作用了。 它允許你定義一個 intersectionRatio 臨界值。 每次 intersectionRatio
經過這些值的時候,你的回調函數都會被調用。threshold
的默認值是[0]
,就是默認行為。 如果我們把 threshold
改為 [0, 0.25, 0.5, 0.75, 1]
,當元素的每四分之一變為可見時,我們都會收到通知:
還有其他別的屬性嗎?
到目前為止,僅剩一個屬性沒在上文列出。 rootMargin 允許你指定到跟元素的距離,允許你有效的擴大或縮小交叉區域面積。 這些 margin 使用 CSS 風格的字符串,例如 10px 20px 30px 40px
,依次指定上、右、下、左邊距。 總結一下,IntersectionObservers
結構提供了如下選項:
new IntersectionObserver(entries => {/* … */}, { // 用於計算相交區域的根元素 // 如果未提供,使用最上級文檔的可見窗口 root: null, // 同 margin,可以是 1、2、3、4 個值,允許時負值。 // 如果顯式指定了跟元素,該值可以使用百分比,即根元素大小的百分之多少。 // 如果沒指定根元素,使用百分比會出錯。 rootMargin: "0px", // 觸發回調函數的臨界值,用 0 ~ 1 的比率指定,也可以是一個數組。 // 其值是被觀測元素可視面積 / 總面積。 // 當可視比率經過這個值的時候,回調函數就會被調用。 threshold: [0], });
iframe 魔法
設計 IntersectionObserver 的時候,我們着重考慮了廣告服務和社交網絡組件的需要,他們經常會內嵌在 iframe 里。使用 IntersectionObserver 可以簡單的知道他們是否可見。 如果 iframe 在觀測它內部的某個元素,滾動 iframe 本身或者滾動 iframe 外層的窗口都會在何時的時間觸發回調函數。 在后一種情況,rootBounds
應該被設置為 null
以防止跨域泄漏數據。
IntersectionObserver 不是干什么的
有一點要記住:IntersectionObserver 不是完美精確到像素級別,也不是低延時性的。 使用它實現類似依賴滾動效果的動畫注定會失敗,因為回調函數被調用的時候那些數據——嚴格來說——已經過期了。 這篇說明 有更多IntersectionObserver
的用法細節。
我在回調函數中可以做多少工作?
簡單的說:在回調函數中花大量時間會讓你的 app 卡頓。所有的最佳實踐在這里都適用。
去使用它吧
瀏覽器對於 IntersectionObserver
的支持度仍然較低,它不會再每個地方都正常工作。 在此期間,WICG 正在給它開發 polyfill。 當然,你沒法獲得原生實現給你的性能提升。