本文是深入淺出 ahooks 源碼系列文章的第十三篇,該系列已整理成文檔-地址。覺得還不錯,給個 star 支持一下哈,Thanks。 本篇文章探討一下 ahooks 對 DOM 類 Hooks 使用規範,以及源碼中是如何去做處理的。 DOM 類 Hooks 使用規範 這一章節,大部分參考官方文檔的 ...
本文是深入淺出 ahooks 源碼系列文章的第十三篇,該系列已整理成文檔-地址。覺得還不錯,給個 star 支持一下哈,Thanks。
本篇文章探討一下 ahooks 對 DOM 類 Hooks 使用規範,以及源碼中是如何去做處理的。
DOM 類 Hooks 使用規範
這一章節,大部分參考官方文檔的 DOM 類 Hooks 使用規範。
第一點,ahooks 大部分 DOM 類 Hooks 都會接收 target 參數,表示要處理的元素。
target 支持三種類型 React.MutableRefObject(通過 useRef 保存的 DOM)、HTMLElement、() => HTMLElement(一般運用於 SSR 場景)。
第二點,DOM 類 Hooks 的 target 是支持動態變化的。如下所示:
export default () => {
const [boolean, { toggle }] = useBoolean();
const ref = useRef(null);
const ref2 = useRef(null);
const isHovering = useHover(boolean ? ref : ref2);
return (
<>
<div ref={ref}>{isHovering ? 'hover' : 'leaveHover'}</div>
<div ref={ref2}>{isHovering ? 'hover' : 'leaveHover'}</div>
</>
);
};
那 ahooks 是怎麼處理這兩點的呢?
getTargetElement
獲取到對應的 DOM 元素,這一點主要相容以上第一點的入參規範。
- 假如是函數,則取執行完後的結果。
- 假如擁有 current 屬性,則取 current 屬性的值,相容
React.MutableRefObject
類型。 - 最後就是普通的 DOM 元素。
export function getTargetElement<T extends TargetType>(target: BasicTarget<T>, defaultElement?: T) {
// 省略部分代碼...
let targetElement: TargetValue<T>;
if (isFunction(target)) {
// 支持函數獲取
targetElement = target();
// 假如 ref,則返回 current
} else if ('current' in target) {
targetElement = target.current;
// 支持 DOM
} else {
targetElement = target;
}
return targetElement;
}
useEffectWithTarget
這個方法,主要是為了支持第二點,支持 target 動態變化。
其中 packages/hooks/src/utils/useEffectWithTarget.ts
是使用 useEffect。
import { useEffect } from 'react';
import createEffectWithTarget from './createEffectWithTarget';
const useEffectWithTarget = createEffectWithTarget(useEffect);
export default useEffectWithTarget;
另外 其中 packages/hooks/src/utils/useLayoutEffectWithTarget.ts
是使用 useLayoutEffect。
import { useLayoutEffect } from 'react';
import createEffectWithTarget from './createEffectWithTarget';
const useEffectWithTarget = createEffectWithTarget(useLayoutEffect);
export default useEffectWithTarget;
兩者都是調用的 createEffectWithTarget,只是入參不同。
直接重點看這個 createEffectWithTarget 函數:
- createEffectWithTarget 返回的函數 useEffectWithTarget 接受三個參數,前兩個跟 useEffect 一樣,第三個就是 target。
- useEffectType 就是 useEffect 或者 useLayoutEffect。註意這裡調用的時候,沒傳第二個參數,也就是每次都會執行。
- hasInitRef 判斷是否已經初始化。lastElementRef 記錄的是最後一次 target 元素的列表。lastDepsRef 記錄的是最後一次的依賴。unLoadRef 是執行完 effect 函數(對應的就是 useEffect 中的 effect 函數)的返回值,在組件卸載的時候執行。
- 第一次執行的時候,執行相應的邏輯,並記錄下最後一次執行的相應的 target 元素以及依賴。
- 後面每次執行的時候,都判斷目標元素或者依賴是否發生變化,發生變化,則執行對應的 effect 函數。並更新最後一次執行的依賴。
- 組件卸載的時候,執行 unLoadRef.current?.() 函數,並重置 hasInitRef 為 false。
const createEffectWithTarget = (useEffectType: typeof useEffect | typeof useLayoutEffect) => {
/**
* @param effect
* @param deps
* @param target target should compare ref.current vs ref.current, dom vs dom, ()=>dom vs ()=>dom
*/
const useEffectWithTarget = (
effect: EffectCallback,
deps: DependencyList,
target: BasicTarget<any> | BasicTarget<any>[],
) => {
const hasInitRef = useRef(false);
const lastElementRef = useRef<(Element | null)[]>([]);
const lastDepsRef = useRef<DependencyList>([]);
const unLoadRef = useRef<any>();
// useEffect 或者 useLayoutEffect
useEffectType(() => {
// 處理 DOM 目標元素
const targets = Array.isArray(target) ? target : [target];
const els = targets.map((item) => getTargetElement(item));
// init run
// 首次初始化的時候執行
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?.();
// for react-refresh
hasInitRef.current = false;
});
};
return useEffectWithTarget;
};
思考與總結
一個優秀的工具庫應該有自己的一套輸入輸出規範,一來能夠支持更多的場景,二來可以更好的在內部進行封裝處理,三來使用者能夠更加快速熟悉和使用相應的功能,能做到舉一反三。
本文已收錄到個人博客中,歡迎關註~