前言
本文是 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
本文主要解讀 useTrackedEffect、useWhyDidYouUpdate 的源碼實現
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 方法實現:
- 對前後兩個 deps 依賴項列表使用 Object.is 進行嚴格相等性檢查
-
如果定義了 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會告訴我們監聽數據中所有變化的數據,不管它是不是無效的更新,但還需要我們自己來區分識別哪些是無效更新的屬性,從而進行優化。
實現思路
- 使用 useRef 聲明 prevProps 變量(確保拿到最新值),用來保存上一次的 props
- 每次 useEffect 更新都置空 changedProps 對象,並將新舊 props 對象的屬性提取出來,生成屬性數組 allKeys
- 遍歷 allKeys 數組,去對比新舊屬性值。如果不同,則記錄到 changedProps 對象中
- 如果 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;
});
}
完整源碼