DOM在Ahooks中的處理過程

大雄45發表於2022-07-21
導讀 一個優秀的工具庫應該有自己的一套輸入輸出規範,一來能夠支援更多的場景,二來可以更好的在內部進行封裝處理,三來使用者能夠更加快速熟悉和使用相應的功能,能做到舉一反三。

本篇文章探討一下 ahooks 對 DOM 類 Hooks 使用規範,以及原始碼中是如何去做處理的。
DOM在Ahooks中的處理過程DOM在Ahooks中的處理過程

DOM 類 Hooks 使用規範

這一章節,大部分參考官方文件的 DOM 類 Hooks 使用規範[1]。

第一點,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 (
    <>{isHovering ? 'hover' : 'leaveHover'}{isHovering ? 'hover' : 'leaveHover'});
};

那 ahooks 是怎麼處理這兩點的呢?

getTargetElement

獲取到對應的 DOM 元素,這一點主要相容以上第一點的入參規範。

假如是函式,則取執行完後的結果。
假如擁有 current 屬性,則取 current 屬性的值,相容React.MutableRefObject 型別。
最後就是普通的 DOM 元素。

export function getTargetElement(target: BasicTarget, defaultElement?: T) {
  // 省略部分程式碼...
  let targetElement: TargetValue;
  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| BasicTarget[],
  ) => {
    const hasInitRef = useRef(false);
    const lastElementRef = useRef<(Element | null)[]>([]);
    const lastDepsRef = useRef([]);
    const unLoadRef = useRef();
    // 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;
};
思考與總結

一個優秀的工具庫應該有自己的一套輸入輸出規範,一來能夠支援更多的場景,二來可以更好的在內部進行封裝處理,三來使用者能夠更加快速熟悉和使用相應的功能,能做到舉一反三。

原文來自:

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69955379/viewspace-2906855/,如需轉載,請註明出處,否則將追究法律責任。

相關文章