本文是深入淺出 ahooks 源碼系列文章的第十三篇,這個系列的目標主要有以下幾點:
學習如何抽象自定義 hooks。構建屬於自己的 React hooks 工具庫。培養閱讀學習源碼的習慣,工具庫是一個對源碼閱讀不錯的選擇。本篇文章探討一下 ahooks 對 DOM 類 Hooks 使用規範,以及源碼中是如何去做處理的。
DOM 類 Hooks 使用規範 這一章節,大部分參考官方文檔的 DOM 類 Hooks 使用規範[1]。
第一點,ahooks 大部分 DOM 類 Hooks 都會接收 target 參數,表示要處理的元素。
target 支持三種類型 React.MutableRefObject(通過 useRef 保存的 DOM)、HTMLElement、() => HTMLElement(一般運用於 SSR 場景)。
第二點,DOM 類 Hooks 的 target 是支持動態變化的。如下所示:
exportdefault()=>{const[boolean,{toggle}]=useBoolean();constref=useRef(null);constref2=useRef(null);constisHovering=useHover(boolean?ref:ref2);return(<><divref={ref}>{isHovering?'hover':'leaveHover'}</div><divref={ref2}>{isHovering?'hover':'leaveHover'}</div></>);};那 ahooks 是怎麼處理這兩點的呢?
getTargetElement 獲取到對應的 DOM 元素,這一點主要兼容以上第一點的入參規範。
假如擁有 current 屬性,則取 current 屬性的值,兼容 React.MutableRefObject 類型。exportfunctiongetTargetElement<TextendsTargetType>(target:BasicTarget<T>,defaultElement?:T){//省略部分代碼...lettargetElement:TargetValue<T>;if(isFunction(target)){//支持函數獲取targetElement=target();//假如ref,則返回current}elseif('current'intarget){targetElement=target.current;//支持DOM}else{targetElement=target;}returntargetElement;}useEffectWithTarget 這個方法,主要是為了支持第二點,支持 target 動態變化。
其中 packages/hooks/src/utils/useEffectWithTarget.ts 是使用 useEffect。
import{useEffect}from'react';importcreateEffectWithTargetfrom'./createEffectWithTarget';constuseEffectWithTarget=createEffectWithTarget(useEffect);exportdefaultuseEffectWithTarget;另外 其中 packages/hooks/src/utils/useLayoutEffectWithTarget.ts 是使用 useLayoutEffect。
import{useLayoutEffect}from'react';importcreateEffectWithTargetfrom'./createEffectWithTarget';constuseEffectWithTarget=createEffectWithTarget(useLayoutEffect);exportdefaultuseEffectWithTarget;兩者都是調用的 createEffectWithTarget,只是入參不同。
直接重點看這個 createEffectWithTarget 函數:
createEffectWithTarget 返回的函數 useEffectWithTarget 接受三個參數,前兩個跟 useEffect 一樣,第三個就是 target。useEffectType 就是 useEffect 或者 useLayoutEffect。注意這裡調用的時候,沒傳第二個參數,也就是每次都會執行。hasInitRef 判斷是否已經初始化。lastElementRef 記錄的是最後一次 target 元素的列表。lastDepsRef 記錄的是最後一次的依賴。unLoadRef 是執行完 effect 函數(對應的就是 useEffect 中的 effect 函數)的返回值,在組件卸載的時候執行。第一次執行的時候,執行相應的邏輯,並記錄下最後一次執行的相應的 target 元素以及依賴。後面每次執行的時候,都判斷目標元素或者依賴是否發生變化,發生變化,則執行對應的 effect 函數。並更新最後一次執行的依賴。組件卸載的時候,執行 unLoadRef.current?.() 函數,並重置 hasInitRef 為 false。constcreateEffectWithTarget=(useEffectType:typeofuseEffect|typeofuseLayoutEffect)=>{/***@parameffect*@paramdeps*@paramtargettargetshouldcompareref.currentvsref.current,domvsdom,()=>domvs()=>dom*/constuseEffectWithTarget=(effect:EffectCallback,deps:DependencyList,target:BasicTarget<any>|BasicTarget<any>[],)=>{consthasInitRef=useRef(false);constlastElementRef=useRef<(Element|null)[]>([]);constlastDepsRef=useRef<DependencyList>([]);constunLoadRef=useRef<any>();//useEffect或者useLayoutEffectuseEffectType(()=>{//處理DOM目標元素consttargets=Array.isArray(target)?target:[target];constels=targets.map((item)=>getTargetElement(item));//initrun//首次初始化的時候執行if(!hasInitRef.current){hasInitRef.current=true;lastElementRef.current=els;lastDepsRef.current=deps;//執行回調中的effect函數unLoadRef.current=effect();return;}//非首次執行的邏輯if(//目標元素或者依賴發生變化els.length!==lastElementRef.current.length||!depsAreSame(els,lastElementRef.current)||!depsAreSame(deps,lastDepsRef.current)){//執行上次返回的結果unLoadRef.current?.();//更新lastElementRef.current=els;lastDepsRef.current=deps;unLoadRef.current=effect();}});useUnmount(()=>{//卸載unLoadRef.current?.();//forreact-refreshhasInitRef.current=false;});};returnuseEffectWithTarget;};思考與總結 一個優秀的工具庫應該有自己的一套輸入輸出規範,一來能夠支持更多的場景,二來可以更好的在內部進行封裝處理,三來使用者能夠更加快速熟悉和使用相應的功能,能做到舉一反三。
參考資料[1]DOM 類 Hooks 使用規範: https://ahooks.js.org/zh-CN/guide/dom