博客 / 詳情

返回

【解讀 ahooks 源碼系列】Dev篇——useTrackedEffect 和 useWhyDidYouUpdate

前言

本文是 ahooks 源碼(v3.7.4)系列的第六篇——Dev 篇,該篇主要是協助開發調優的 Hook,只有兩個

往期文章:

  • 【解讀 ahooks 源碼系列】(開篇)如何獲取和監聽 DOM 元素:useEffectWithTarget
  • 【解讀 ahooks 源碼系列】DOM 篇(一):useEventListener、useClickAway、useDocumentVisibility、useDrop、useDrag
  • 【解讀 ahooks 源碼系列】DOM 篇(二):useEventTarget、useExternal、useTitle、useFavicon、useFullscreen、useHover
  • 【解讀 ahooks 源碼系列】DOM 篇(三):useMutationObserver、useInViewport、useKeyPress、useLongPress
  • 【解讀 ahooks 源碼系列】DOM 篇(四):useMouse、useResponsive、useScroll、useSize、useFocusWithin

本文主要解讀 useTrackedEffectuseWhyDidYouUpdate 的源碼實現

useTrackedEffect

追蹤是哪個依賴變化觸發了 useEffect 的執行。

官方文檔

基本用法

查看每次 effect 執行時發生變化的依賴項

官方在線 Demo

import React, { useState } from 'react';
import { useTrackedEffect } from 'ahooks';

export default () => {
  const [count, setCount] = useState(0);
  const [count2, setCount2] = useState(0);

  useTrackedEffect(
    (changes) => {
      console.log('Index of changed dependencies: ', changes);
    },
    [count, count2],
  );

  return (
    <div>
      <p>Please open the browser console to view the output!</p>
      <div>
        <p>Count: {count}</p>
        <button onClick={() => setCount((c) => c + 1)}>count + 1</button>
      </div>
      <div style={{ marginTop: 16 }}>
        <p>Count2: {count2}</p>
        <button onClick={() => setCount2((c) => c + 1)}>count + 1</button>
      </div>
    </div>
  );
};

核心實現

實現原理:通過 uesRef 記錄上一次依賴的值,在當前執行的時候,判斷當前依賴值和上次依賴值之間有無變化

  • changes:變化的依賴 index 數組
  • previousDeps:上一個依賴
  • currentDeps:當前依賴
useTrackedEffect(
  effect: (changes: [], previousDeps: [], currentDeps: []) => (void | (() => void | undefined)),
  deps?: deps,
)

源碼實現

const useTrackedEffect = (effect: Effect, deps?: DependencyList) => {
  const previousDepsRef = useRef<DependencyList>(); // 記錄上次依賴

  useEffect(() => {
    // 判斷依賴前後的 changes
    const changes = diffTwoDeps(previousDepsRef.current, deps);
    const previousDeps = previousDepsRef.current; // 賦值上次依賴
    previousDepsRef.current = deps;
    return effect(changes, previousDeps, deps);
  }, deps);
};

diffTwoDeps 方法實現:

  1. 對前後兩個 deps 依賴項列表使用 Object.is 進行嚴格相等性檢查
  2. 如果定義了 deps1,則遍歷 deps1 並將每個元素與來自 deps2 的對應索引元素進行比較(因為這個函數只在這個鈎子中使用,所以假設兩個 deps 列表的長度總是相同的)

    • 相等返回 -1
    • 不相等返回索引值
    • 過濾小於 0 的值(即校驗結果相等的).filter((ele) => ele >= 0),最終只返回變化的數組索引值
const diffTwoDeps = (deps1?: DependencyList, deps2?: DependencyList) => {
  // 對前後兩個 deps 依賴項列表使用 Object.is 進行嚴格相等性檢查
  return deps1
    ? deps1
        .map((_ele, idx) => (!Object.is(deps1[idx], deps2?.[idx]) ? idx : -1))
        .filter((ele) => ele >= 0) // 過濾相等值
    : deps2
    ? deps2.map((_ele, idx) => idx)
    : [];
};

完整源碼

useWhyDidYouUpdate

幫助開發者排查是那個屬性改變導致了組件的 rerender。

官方文檔

基本用法

官方在線 Demo

打開控制枱,可以看到改變的屬性。

import { useWhyDidYouUpdate } from 'ahooks';
import React, { useState } from 'react';

const Demo: React.FC<{ count: number }> = (props) => {
  const [randomNum, setRandomNum] = useState(Math.random());

  useWhyDidYouUpdate('useWhyDidYouUpdateComponent', { ...props, randomNum });

  return (
    <div>
      <div>
        <span>number: {props.count}</span>
      </div>
      <div>
        randomNum: {randomNum}
        <button onClick={() => setRandomNum(Math.random)} style={{ marginLeft: 8 }}>
          🎲
        </button>
      </div>
    </div>
  );
};

export default () => {
  const [count, setCount] = useState(0);

  return (
    <div>
      <Demo count={count} />
      <div>
        <button onClick={() => setCount((prevCount) => prevCount - 1)}>count -</button>
        <button onClick={() => setCount((prevCount) => prevCount + 1)} style={{ marginLeft: 8 }}>
          count +
        </button>
      </div>
      <p style={{ marginTop: 8 }}>Please open the browser console to view the output!</p>
    </div>
  );
};

使用場景

  • 檢查哪些 props 發生改變
  • 協助找出無效渲染:useWhyDidYouUpdate 會告訴我們監聽數據中所有變化的數據,不管它是不是無效的更新,但還需要我們自己來區分識別哪些是無效更新的屬性,從而進行優化。

實現思路

  1. 使用 useRef 聲明 prevProps 變量(確保拿到最新值),用來保存上一次的 props
  2. 每次 useEffect 更新都置空 changedProps 對象,並將新舊 props 對象的屬性提取出來,生成屬性數組 allKeys
  3. 遍歷 allKeys 數組,去對比新舊屬性值。如果不同,則記錄到 changedProps 對象中
  4. 如果 changedProps 有長度,則輸出改變的內容,並更新 prevProps

核心實現

實現原理:通過 useEffect 拿到上一次 props 值 和當前 props 值 進行遍歷比較,如果值發送改變則輸出

// componentName:觀測組件的名稱
// props:需要觀測的數據(當前組件 state 或者傳入的 props 等可能導致 rerender 的數據)
export default function useWhyDidYouUpdate(componentName: string, props: IProps) {
  const prevProps = useRef<IProps>({});

  useEffect(() => {
    if (prevProps.current) {
      // 獲取所有的需要觀測的數據
      const allKeys = Object.keys({ ...prevProps.current, ...props });
      const changedProps: IProps = {}; // 發生改變的屬性值

      allKeys.forEach((key) => {
        // 通過 Object.is 判斷是否進行更新
        if (!Object.is(prevProps.current[key], props[key])) {
          changedProps[key] = {
            from: prevProps.current[key],
            to: props[key],
          };
        }
      });

      // 遍歷改變的屬性,有值則輸出日誌
      if (Object.keys(changedProps).length) {
        console.log('[why-did-you-update]', componentName, changedProps);
      }
    }

    prevProps.current = props;
  });
}

完整源碼

user avatar laughingzhu 頭像 peter-wilson 頭像 icecreamlj 頭像 uncletong_doge 頭像 zhongyuandaxia 頭像 xiaohaiqianduan 頭像 huaihuaidedianti 頭像 xuriliang 頭像 tempest_619c7f9d4e321 頭像 yihan123 頭像 xiaohuoche 頭像 denzel 頭像
19 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.