IntersectionObserver 能夠輕鬆判斷元素是否可見,在以前的 精讀《用 React 作按需渲染》 中介紹了原生 API 的方法,此次恰好看到其 React 封裝版本 react-intersection-observer,讓咱們看一看 React 封裝思路。前端
react-intersection-observer 提供了 Hook useInView
判斷元素是否在可視區域內,API 以下:node
import React from "react"; import { useInView } from "react-intersection-observer"; const Component = () => { const [ref, inView] = useInView(); return ( <div ref={ref}> <h2>{`Header inside viewport ${inView}.`}</h2> </div> ); };
因爲判斷元素是否可見是基於 dom 的,因此必須將 ref
回調函數傳遞給 表明元素輪廓的 DOM 元素,上面的例子中,咱們將 ref
傳遞給了最外層 DIV。react
useInView
還支持下列參數:git
root
:檢測是否可見基於的視窗元素,默認是整個瀏覽器 viewport。rootMargin
:root 邊距,能夠在檢測時提早或者推遲固定像素判斷。threshold
:是否可見的閾值,範圍 0 ~ 1,0 表示任意可見即爲可見,1 表示徹底可見即爲可見。triggerOnce
:是否僅觸發一次。首先從入口函數 useInView
開始解讀,這是一個 Hook,利用 ref
存儲上一次 DOM 實例,state
則存儲 inView
元素是否可見的 boolean 值:github
export function useInView( options: IntersectionOptions = {}, ): InViewHookResponse { const ref = React.useRef<Element>() const [state, setState] = React.useState<State>(initialState) // 中間部分.. return [setRef, state.inView, state.entry] }
當組件 ref 被賦值時會調用 setRef
,回調 node
是新的 DOM 節點,所以先 unobserve(ref.current)
取消舊節點的監聽,再 observe(node)
對新節點進行監聽,最後 ref.current = node
更新舊節點:數組
// 中間部分 1 const setRef = React.useCallback( (node) => { if (ref.current) { unobserve(ref.current); } if (node) { observe( node, (inView, intersection) => { setState({ inView, entry: intersection }); if (inView && options.triggerOnce) { // If it should only trigger once, unobserve the element after it's inView unobserve(node); } }, options ); } // Store a reference to the node, so we can unobserve it later ref.current = node; }, [options.threshold, options.root, options.rootMargin, options.triggerOnce] );
另外一段是,當 ref
不存在時會清空 inView
狀態,畢竟當不存在監聽對象時,inView 值只有重設爲默認 false 才合理:瀏覽器
// 中間部分 2 useEffect(() => { if (!ref.current && state !== initialState && !options.triggerOnce) { // If we don't have a ref, then reset the state (unless the hook is set to only `triggerOnce`) // This ensures we correctly reflect the current state - If you aren't observing anything, then nothing is inView setState(initialState); } });
這就是入口文件的邏輯,咱們能夠看到還有兩個重要的函數 observe
與 unobserve
,這兩個函數的實如今 intersection.ts 文件中,這個文件有三個核心函數:observe
、unobserve
、onChange
。微信
observe
:監聽 element 是否在可視區域。unobserve
:取消監聽。onChange
:處理 observe
變化的回調。先看 observe
,對於同一個 root 下的監聽會作合併操做,所以須要生成 observerId
做爲惟一標識,這個標識由 getRootId
、rootMargin
、threshold
共同決定。框架
對於同一個 root 的監聽下,拿到 new IntersectionObserver()
建立的 observerInstance
實例,調用 observerInstance.observe
進行監聽。這裏存儲了兩個 Map - OBSERVER_MAP
與 INSTANCE_MAP
,前者是保證同一 root 下 IntersectionObserver
實例惟一,後者存儲了組件 inView
以及回調等信息,在 onChange
函數使用:less
export function observe( element: Element, callback: ObserverInstanceCallback, options: IntersectionObserverInit = {} ) { // IntersectionObserver needs a threshold to trigger, so set it to 0 if it's not defined. // Modify the options object, since it's used in the onChange handler. if (!options.threshold) options.threshold = 0; const { root, rootMargin, threshold } = options; // Validate that the element is not being used in another <Observer /> invariant( !INSTANCE_MAP.has(element), "react-intersection-observer: Trying to observe %s, but it's already being observed by another instance.\nMake sure the `ref` is only used by a single <Observer /> instance.\n\n%s" ); /* istanbul ignore if */ if (!element) return; // Create a unique ID for this observer instance, based on the root, root margin and threshold. // An observer with the same options can be reused, so lets use this fact let observerId: string = getRootId(root) + (rootMargin ? `${threshold.toString()}_${rootMargin}` : threshold.toString()); let observerInstance = OBSERVER_MAP.get(observerId); if (!observerInstance) { observerInstance = new IntersectionObserver(onChange, options); /* istanbul ignore else */ if (observerId) OBSERVER_MAP.set(observerId, observerInstance); } const instance: ObserverInstance = { callback, element, inView: false, observerId, observer: observerInstance, // Make sure we have the thresholds value. It's undefined on a browser like Chrome 51. thresholds: observerInstance.thresholds || (Array.isArray(threshold) ? threshold : [threshold]), }; INSTANCE_MAP.set(element, instance); observerInstance.observe(element); return instance; }
對於 onChange
函數,由於採用了多元素監聽,因此須要遍歷 changes
數組,並判斷 intersectionRatio
超過閾值斷定爲 inView
狀態,經過 INSTANCE_MAP
拿到對應實例,修改其 inView
狀態並執行 callback
。
這個 callback
就對應了 useInView
Hook 中 observe
的第二個參數回調:
function onChange(changes: IntersectionObserverEntry[]) { changes.forEach((intersection) => { const { isIntersecting, intersectionRatio, target } = intersection; const instance = INSTANCE_MAP.get(target); // Firefox can report a negative intersectionRatio when scrolling. /* istanbul ignore else */ if (instance && intersectionRatio >= 0) { // If threshold is an array, check if any of them intersects. This just triggers the onChange event multiple times. let inView = instance.thresholds.some((threshold) => { return instance.inView ? intersectionRatio > threshold : intersectionRatio >= threshold; }); if (isIntersecting !== undefined) { // If isIntersecting is defined, ensure that the element is actually intersecting. // Otherwise it reports a threshold of 0 inView = inView && isIntersecting; } instance.inView = inView; instance.callback(inView, intersection); } }); }
最後是 unobserve
取消監聽的實現,在 useInView
setRef
灌入新 Node 節點時,會調用 unobserve
對舊節點取消監聽。
首先利用 INSTANCE_MAP
找到實例,調用 observer.unobserve(element)
銷燬監聽。最後銷燬沒必要要的 INSTANCE_MAP
與 ROOT_IDS
存儲。
export function unobserve(element: Element | null) { if (!element) return; const instance = INSTANCE_MAP.get(element); if (instance) { const { observerId, observer } = instance; const { root } = observer; observer.unobserve(element); // Check if we are still observing any elements with the same threshold. let itemsLeft = false; // Check if we still have observers configured with the same root. let rootObserved = false; /* istanbul ignore else */ if (observerId) { INSTANCE_MAP.forEach((item, key) => { if (key !== element) { if (item.observerId === observerId) { itemsLeft = true; rootObserved = true; } if (item.observer.root === root) { rootObserved = true; } } }); } if (!rootObserved && root) ROOT_IDS.delete(root); if (observer && !itemsLeft) { // No more elements to observe for threshold, disconnect observer observer.disconnect(); } // Remove reference to element INSTANCE_MAP.delete(element); } }
從其實現角度來看,爲了保證正確識別到子元素存在,必定要保證 ref
能持續傳遞給組件最外層 DOM,若是出現傳遞斷裂,就會斷定當前組件不在視圖內,好比:
const Component = () => { const [ref, inView] = useInView(); return <Child ref={ref} />; }; const Child = ({ loading, ref }) => { if (loading) { // 這一步會斷定爲 inView:false return <Spin />; } return <div ref={ref}>Child</div>; };
若是你的代碼基於 inView
作了阻止渲染的斷定,那麼這個組件進入 loading 後就沒法改變狀態了。爲了不這種狀況,要麼不要讓 ref
的傳遞斷掉,要麼當沒有拿到 ref
對象時斷定 inView
爲 true。
分析了這麼多 React- 類的庫,其核心思想有兩個:
unobserve
再從新 observe
。看過 react-intersection-observer 的源碼後,你以爲還有可優化的地方嗎?歡迎討論。
討論地址是: react-intersection-observer 源碼》· Issue #257 · dt-fe/weekly
若是你想參與討論,請 點擊這裏,每週都有新的主題,週末或週一發佈。前端精讀 - 幫你篩選靠譜的內容。
關注 前端精讀微信公衆號
版權聲明:自由轉載-非商用-非衍生-保持署名( 創意共享 3.0 許可證)
本文使用 mdnice 排版