Ahooks 中那些控制“時(shí)機(jī)”的 Hook 都是怎么實(shí)現(xiàn)的?
本文是深入淺出 ahooks 源碼系列,這個(gè)系列的目標(biāo)主要有以下幾點(diǎn):
加深對 React hooks 的理解。
學(xué)習(xí)如何抽象自定義 hooks。構(gòu)建屬于自己的 React hooks 工具庫。
培養(yǎng)閱讀學(xué)習(xí)源碼的習(xí)慣,工具庫是一個(gè)對源碼閱讀不錯(cuò)的選擇。
注:本系列對 ahooks 的源碼解析是基于 v3.3.13。自己 folk 了一份源碼,主要是對源碼做了一些解讀,可見 詳情[1]。
本文來探索一下 ahooks 是怎么封裝 React 的一些執(zhí)行“時(shí)機(jī)”的?
Function Component VS Class Component
學(xué)習(xí)類似 React 和 Vue 這種框架,對它們生命周期的掌握都是必須的,我們需要清楚的知道我們代碼的執(zhí)行順序,并且在不同的階段執(zhí)行不同操作的代碼,比如需要掛載完成之后才去獲取 dom 的值,否則可能會獲取不到相應(yīng)的值。
Class Component
使用過 React 的 Class Component 的同學(xué),就會知道其組件生命周期會分成三個(gè)狀態(tài):
- Mounting(掛載):已插入真實(shí) DOM
- Updating(更新):正在被重新渲染
- Unmounting(卸載):已移出真實(shí) DOM
簡單版如下所示:
其中每個(gè)狀態(tài)中還會按順序調(diào)用不同的方法,對應(yīng)的詳細(xì)如下(這里不展開說):
可以通過官方提供這個(gè)網(wǎng)站查看詳情[2]
可以看到,會有非常多的生命周期方法,而且在不同的版本,生命周期方法還不同。
Function Component
到了 Function Component ,會發(fā)現(xiàn)沒有直接提及生命周期的概念,它是更徹底的狀態(tài)驅(qū)動,它只有一個(gè)狀態(tài),React 負(fù)責(zé)將狀態(tài)渲染到視圖中。
對于 Function Component 來說由狀態(tài)到頁面渲染只有三步:
- 輸入狀態(tài)(prop、state)
- 執(zhí)行組件的邏輯,并在 ?useEffect/useLayoutEffect 中訂閱副作用
- 輸出UI(Dom節(jié)點(diǎn))
重點(diǎn)是第二步,React 通過 useEffect/useLayoutEffect 訂閱副作用。Class Component 中的生命周期都可以通過 useEffect/useLayoutEffect 來實(shí)現(xiàn)。它們兩個(gè)的功能非常相似,我們這里看下 useEffect。
使用 useEffect 相當(dāng)于告訴 React 組件需要在渲染后執(zhí)行某些操作,React 將在執(zhí)行 DOM 更新之后調(diào)用它。React 保證了每次運(yùn)行 useEffect 的時(shí)候,DOM 已經(jīng)更新完畢。這就實(shí)現(xiàn)了 Class Component 中的 Mounting(掛載階段)。
當(dāng)狀態(tài)發(fā)生變化的時(shí)候,它能夠執(zhí)行對應(yīng)的邏輯、更行狀態(tài)并將結(jié)果渲染到視圖中,這就完成了 Class Component 中的 Updating(更新階段)。
最后通過在 useEffect 中返回一個(gè)函數(shù),它便可以清理副作用。它的規(guī)則是:
- 首次渲染不會進(jìn)行清理,會在下一次渲染,清除上一次的副作用。
- 卸載階段也會執(zhí)行清除操作。
通過返回一個(gè)函數(shù),我們就能實(shí)現(xiàn) Class Component 中的 Unmounting(卸載階段)。
基于 useEffect/useLayoutEffect,ahooks 做了一些封裝,能夠讓你更加清晰的知道你的代碼執(zhí)行時(shí)機(jī)。
LifeCycle - 生命周期
useMount
只在組件初始化時(shí)執(zhí)行的 Hook。useEffect 依賴假如為空,只會在組件初始化的時(shí)候執(zhí)行。
// 省略部分代碼
const useMount = (fn: () => void) => {
// 省略部分代碼
// 單純就在 useEffect 基礎(chǔ)上封裝了一層
useEffect(() => {
fn?.();
}, []);
};
export default useMount;
useUnmount
useUnmount,組件卸載(unmount)時(shí)執(zhí)行的 Hook。
useEffect 可以在組件渲染后實(shí)現(xiàn)各種不同的副作用。有些副作用可能需要清除,所以需要返回一個(gè)函數(shù),這個(gè)函數(shù)會在組件卸載的時(shí)候執(zhí)行。
const useUnmount = (fn: () => void) => {
const fnRef = useLatest(fn);
useEffect(
// 在組件卸載(unmount)時(shí)執(zhí)行的 Hook。
// useEffect 的返回值中執(zhí)行函數(shù)
() => () => {
fnRef.current();
},
[],
);
};
export default useUnmount;
useUnmountedRef
獲取當(dāng)前組件是否已經(jīng)卸載的 Hook。
通過判斷有沒有執(zhí)行 useEffect 中的返回值判斷當(dāng)前組件是否已經(jīng)卸載。
// 獲取當(dāng)前組件是否已經(jīng)卸載的 Hook。
const useUnmountedRef = () => {
const unmountedRef = useRef(false);
useEffect(() => {
unmountedRef.current = false;
// 如果已經(jīng)卸載,則會執(zhí)行 return 中的邏輯
return () => {
unmountedRef.current = true;
};
}, []);
return unmountedRef;
};
export default useUnmountedRef;
Effect
這里只會講官方文檔 Effect 下面的幾個(gè),有部分是定時(shí)器、防抖節(jié)流等,咱們后面的系列具體分析。
useUpdateEffect 和 useUpdateLayoutEffect
useUpdateEffect 和 useUpdateLayoutEffect 的用法跟 useEffect 和 useLayoutEffect 一樣,只是會忽略首次執(zhí)行,只在依賴更新時(shí)執(zhí)行。
實(shí)現(xiàn)思路:初始化一個(gè)標(biāo)識符,剛開始為 false。當(dāng)首次執(zhí)行完的時(shí)候,置為 true。只有標(biāo)識符為 true 的時(shí)候,才執(zhí)行回調(diào)函數(shù)。
// 忽略首次執(zhí)行
export const createUpdateEffect: (hook: effectHookType) => effectHookType =
(hook) => (effect, deps) => {
const isMounted = useRef(false);
// for react-refresh
hook(() => {
return () => {
isMounted.current = false;
};
}, []);
hook(() => {
// 首次執(zhí)行完時(shí)候,設(shè)置為 true,從而下次依賴更新的時(shí)候可以執(zhí)行邏輯
if (!isMounted.current) {
isMounted.current = true;
} else {
return effect();
}
}, deps);
};
useDeepCompareEffect和useDeepCompareLayoutEffect
用法與 useEffect 一致,但 deps 通過 lodash isEqual 進(jìn)行深比較。
通過 useRef 保存上一次的依賴的值,跟當(dāng)前的依賴對比(使用 lodash 的 isEqual),并將對比結(jié)果作為 useEffect 的依賴項(xiàng),從而決定回調(diào)函數(shù)是否執(zhí)行。
const depsEqual = (aDeps: DependencyList, bDeps: DependencyList = []) => {
return isEqual(aDeps, bDeps);
};
const useDeepCompareEffect = (effect: EffectCallback, deps: DependencyList) => {
// 通過 useRef 保存上一次的依賴的值
const ref = useRef<DependencyList>();
const signalRef = useRef<number>(0);
// 判斷最新的依賴和舊的區(qū)別
// 如果相等,則變更 signalRef.current,從而觸發(fā) useEffect 中的回調(diào)
if (!depsEqual(deps, ref.current)) {
ref.current = deps;
signalRef.current += 1;
}
useEffect(effect, [signalRef.current]);
};
useUpdate
useUpdate 會返回一個(gè)函數(shù),調(diào)用該函數(shù)會強(qiáng)制組件重新渲染。
返回的函數(shù)通過變更 useState 返回的 state,從而促使組件進(jìn)行更新。
import { useCallback, useState } from 'react';
const useUpdate = () => {
const [, setState] = useState({});
// 通過設(shè)置一個(gè)全新的狀態(tài),促使 function 組件更新
return useCallback(() => setState({}), []);
};
export default useUpdate;
總結(jié)與思考
在我們寫代碼的時(shí)候需要清晰的知道,組件的生命周期是怎樣的,我們代碼的執(zhí)行順序、執(zhí)行的時(shí)機(jī)是怎樣的。
在 Function Component 中,使用 useEffect/useLayoutEffect 完成了 Class Components 生命周期的職責(zé)。ahooks 也是基于這兩個(gè)封裝了常見的代碼執(zhí)行時(shí)機(jī),使用這些 hook,可以讓我們的代碼更加具有可讀性以及邏輯更加清晰。
參考資料
[1]詳情: https://github.com/GpingFeng/hooks
[2]詳情: https://projects.wojtekmaj.pl/react-lifecycle-methods-diagram/
[3]大家都能看得懂的源碼(一)ahooks 整體架構(gòu)篇: https://juejin.cn/post/7105396478268407815
[4]如何使用插件化機(jī)制優(yōu)雅的封裝你的請求hook : https://juejin.cn/post/7105733829972721677
[5]ahooks 是怎么解決 React 的閉包問題的?: https://juejin.cn/post/7106061970184339464
[6]ahooks 是怎么解決用戶多次提交問題?: https://juejin.cn/post/7106461530232717326