(給前端大全加星標,提升前端技能)
作者:flytam
https://juejin.im/post/5dc8f360e51d45782a445204
本文通過對preact的hook源碼分析,理解和掌握react/preact的hook用法以及一些常見的問題。雖然react和preact的實現上有一定的差異,但是對於hook的表現來說,是基本一致的。對於 preact的hook`分析,我們很容易就記住 hook 的使用和防止踩一些誤區。
preact hook 作為一個單獨的包preact/hook引入的,它的總代碼包含注釋區區 300 行。
在閱讀本文之前,先帶著幾個問題閱讀:
1、組件是無狀態的,那麼為什麼 hook 讓它變成了有狀態呢?2、為什麼 hook 不能放在 條件語句裡面3、為什麼不能在普通函數執行 hook
基礎前面提到,hook在preact中是通過preact/hook內一個模塊單獨引入的。這個模塊中有兩個重要的模塊內的全局變量:
1、currentIndex:用於記錄當前函數組件正在使用的 hook 的順序。2、currentComponent。用於記錄當前渲染對應的組件。
preact hook 的實現對於原有的 preact 是幾乎零入侵。它通過暴露在preact.options中的幾個鉤子函數在preact的相應初始/更新時候執行相應的hook邏輯。這幾個鉤子分別是render=>diffed=>commit=>umount
_render位置。執行組件的 render 方法之前執行,用於執行pendingEffects(pendingEffects是不阻塞頁面渲染的 effect 操作,在下一幀繪製前執行)的清理操作和執行未執行的。這個鉤子還有一個很重要的作用就是讓 hook 拿到當前正在執行的render的組件實例
options._render = vnode => {
// render 鉤子函數
if (oldBeforeRender) oldBeforeRender(vnode);
currentComponent = vnode._component;
currentIndex = 0;
if (currentComponent.__hooks) {
// 執行清理操作
currentComponent.__hooks._pendingEffects.forEach(invokeCleanup);
// 執行effect
currentComponent.__hooks._pendingEffects.forEach(invokeEffect);
currentComponent.__hooks._pendingEffects = [];
}
};
結合_render在 preact 的執行時機,可以知道,在這個鉤子函數裡是進行每次 render 的初始化操作。包括執行/清理上次未處理完的 effect、初始化 hook 下標為 0、取得當前 render 的組件實例。
diffed位置。vnode 的 diff 完成之後,將當前的_pendingEffects推進執行隊列,讓它在下一幀繪製前執行,不阻塞本次的瀏覽器渲染。
options.diffed = vnode => {
if (oldAfterDiff) oldAfterDiff(vnode);
const c = vnode._component;
if (!c) return;
const hooks = c.__hooks;
if (hooks) {
// 下面會提到useEffect就是進入_pendingEffects隊列
if (hooks._pendingEffects.length) {
// afterPaint 表示本次幀繪製完,下一幀開始前執行
afterPaint(afterPaintEffects.push(c));
}
}
};
_commit位置。初始或者更新 render 結束之後執行renderCallbacks,在這個_commit中只執行 hook 的回調,如useLayoutEffect。(renderCallbacks是指在preact中指每次 render 後,同步執行的操作回調列表,例如setState的第二個參數 cb、或者一些render後的生命周期函數、或者forceUpdate的回調)。
options._commit = (vnode, commitQueue) => {
commitQueue.some(component => {
// 執行上次的_renderCallbacks的清理函數
component._renderCallbacks.forEach(invokeCleanup);
// _renderCallbacks有可能是setState的第二個參數這種的、或者生命周期、或者forceUpdate的回調。
// 通過_value判斷是hook的回調則在此出執行
component._renderCallbacks = component._renderCallbacks.filter(cb =>
cb._value ? invokeEffect(cb) : true
);
});
if (oldCommit) oldCommit(vnode, commitQueue);
};
unmount。組件的卸載之後執行effect的清理操作
options.unmount = vnode => {
if (oldBeforeUnmount) oldBeforeUnmount(vnode);
const c = vnode._component;
if (!c) return;
const hooks = c.__hooks;
if (hooks) {
// _cleanup 是effect類hook的清理函數,也就是我們每個effect的callback 的返回值函數
hooks._list.forEach(hook => hook._cleanup && hook._cleanup());
}
};
對於組件來說加入的 hook 只是在 preact 的組件基礎上增加一個__hook 屬性。在 preact 的內部實現中,無論是函數組件還是 class 組件, 都是實例化成 PreactComponent,如下數據結構
export interface Component extends PreactComponent<any, any> {
__hooks?: {
// 每個組件的hook存儲
_list: HookState[];
// useLayoutEffect useEffect 等
_pendingEffects: EffectHookState[];
};
}
對於問題 1 的回答,通過上面的分析,我們知道,hook最終是掛在組件的__hooks屬性上的,因此,每次渲染的時候只要去讀取函數組件本身的屬性就能獲取上次渲染的狀態了,就能實現了函數組件的狀態。這裡關鍵在於getHookState這個函數。這個函數也是整個preact hook中非常重要的
function getHookState(index) {
if (options._hook) options._hook(currentComponent);
const hooks =
currentComponent.__hooks ||
(currentComponent.__hooks = { _list: [], _pendingEffects: [] });
// 初始化的時候,創建一個空的hook
if (index >= hooks._list.length) {
hooks._list.push({});
}
return hooks._list[index];
}
這個函數是在組件每次執行useXxx的時候,首先執行這一步獲取 hook 的狀態的(以useEffect為例子)。所有的hook都是使用這個函數先獲取自身 hook 狀態
export function useEffect(callback, args) {
//....
const state = getHookState(currentIndex++);
//
}
這個currentIndex在每一次的render過程中是從 0 開始的,每執行一次useXxx後加一。每個hook在多次render中對於記錄前一次的執行狀態正是通過currentComponent.__hooks中的順序決定。所以如果處於條件語句,如果某一次條件不成立,導致那個useXxx沒有執行,這個後面的 hook 的順序就發生錯亂並導致 bug。
例如
const Component = () => {
const [state1, setState1] = useState();
// 假設condition第一次渲染為true,第二次渲染為false
if (condition) {
const [state2, setState2] = useState();
}
const [state3, setState3] = useState();
};
第一次渲染後,__hooks = [hook1,hook2,hook3]。
第二次渲染,由於const [state2, setState2] = useState();被跳過,通過currentIndex取到的const [state3, setState3] = useState();其實是hook2。就可能有問題。所以,這就是問題 2,為什麼 hook 不能放到條件語句中。
經過上面一些分析,也知道問題 3 為什麼 hook 不能用在普通函數了。因為 hook 都依賴了 hook 內的全局變量currentIndex和currentComponent。而普通函數並不會執行options.render鉤子重置currentIndex和設置currentComponent,當普通函數執行 hook 的時候,currentIndex為上一個執行 hook 組件的實例的下標,currentComponent為上一個執行 hook 組件的實例。因此直接就有問題了。
hook 分析雖然 preact 中的 hook 有很多,數據結構來說只有 3 種HookState結構,所有的 hook 都是在這 3 種的基礎上實現的。這 3 種分別是
EffectHookState (useLayoutEffect useEffect useImperativeHandle)
export interface EffectHookState {
// effect hook的回調函數
_value?: Effect;
// 依賴項
_args?: any[];
// effect hook的清理函數,_value的返回值
_cleanup?: Cleanup;
}
MemoHookState (useMemo useRef useCallback)
export interface MemoHookState {
// useMemo的返回值
_value?: any;
// 前一次的依賴數組
_args?: any[];
//useMemo傳入的callback
_callback?: () => any;
}
ReducerHookState (useReducer useState ``)
export interface ReducerHookState {
_value?: any;
_component?: Component;
}
useContext 這個比較特殊
MemoHookStateMemoHook是一類用來和性能優化有關的 hook
useMemo作用:把創建函數和依賴項數組作為參數傳入 useMemo,它僅會在某個依賴項改變時才重新計算 memoized 值。這種優化有助於避免在每次渲染時都進行高開銷的計算
// 例子
const Component = props => {
// 假設calculate是個消耗很多的計算操作
const result = calculate(props.xx);
return <div>{result}</div>;
};
默認情況下,每次Component渲染都會執行calculate的計算操作,如果calculate是一個大計算量的函數,這裡會有造成性能下降,這裡就可以使用useMemo來進行優化了。這樣如果calculate依賴的值沒有變化,就不需要執行這個函數,而是取它的緩存值。要注意的是calculate對外部依賴的值都需要傳進依賴項數組,否則當部分值變化是,useMemo卻還是舊的值可能會產生 bug。
// 例子
const Component = props => {
// 這樣子,只會在props.xx值改變時才重新執行calculate函數,達到了優化的目的
const result = useMemo(() => calculate(props.xx), [props.xx]);
return <div>{result}</div>;
};
useMemo源碼分析
function useMemo(callback, args) {
// state是MemoHookState類型
const state = getHookState(currentIndex++);
// 判斷依賴項是否改變
if (argsChanged(state._args, args)) {
// 存儲本次依賴的數據值
state._args = args;
state._callback = callback;
// 改變後執行`callback`函數返回值。
return (state._value = callback());
}
return state._value;
}
useMemo的實現邏輯不複雜,判斷依賴項是否改變,改變後執行callback函數返回值。值得一提的是,依賴項比較只是普通的===比較,如果依賴的是引用類型,並且直接改變改引用類型上的屬性,將不會執行callback。
useCallback作用:接收一個內聯回調函數參數和一個依賴項數組(子組件依賴父組件的狀態,即子組件會使用到父組件的值) ,useCallback 會返回該回調函數的 memorized 版本,該回調函數僅在某個依賴項改變時才會更新
假設有這樣一段代碼
// 例子
const Component = props => {
const [number, setNumber] = useState(0);
const handle = () => console.log(number);
return <button onClick={handle}>按鈕</button>;
};
對於每次的渲染,都是新的 handle,因此 diff 都會失效,都會有一個創建一個新的函數,並且綁定新的事件代理的過程。當使用useCallback後則會解決這個問題
// 例子
const Component = props => {
const [number, setNumber] = useState(0);
// 這裡,如果number不變的情況下,每次的handle是同一個值
const handle = useCallback(() => () => console.log(number), [number]);
return <button onClick={handle}>按鈕</button>;
};
有一個坑點是,[number]是不能省略的,如果省略的話,每次列印的log永遠是number的初始值 0
// 例子
const Component = props => {
const [number, setNumber] = useState(0);
// 這裡永遠列印0
const handle = useCallback(() => () => console.log(number), []);
return <button onClick={handle}>按鈕</button>;
};
至於為什麼這樣,結合useMomo的實現分析。useCallback是在useMemo的基礎上實現的,只是它不執行這個 callback,而是返回這個 callback,用於執行。
function useCallback(callback, args) {
// 直接返回這個callback,而不是執行
return useMemo(() => callback, args);
}
我們想像一下,每次的函數組件執行,都是一個全新的過程。而我們的 callback 只是掛在MemoHook的_value欄位上,當依賴沒有改變的時候,我們執行的callback永遠是創建的那個時刻那次渲染的形成的閉包函數。而那個時刻的number就是初次的渲染值。
// 例子
const Component = props => {
const [number, setNumber] = useState(0);
// 這裡永遠列印0
const handle = useCallback(
() => /** 到了後面的時候,我們的handle並不是執行這次的callback,而是上次的那個記錄的callback*/ () =>
console.log(number),
[]
);
return <button onClick={handle}>按鈕</button>;
};
useMemo和useCallback對於性能優化很好用,但是並不是必須的。因為對於大多數的函數來說,一方面創建/調用消耗並不大,而記錄依賴項是需要一個遍歷數組的對比操作,這個也是需要消耗的。因此並不需要無腦useMemo和useCallback,而是在一些剛好的地方使用才行
useRef作用:useRef 返回一個可變的 ref 對象,其 current 屬性被初始化為傳入的參數(initialValue)。就是在函數組件中替代React.createRef的功能或者類似於this.xxx的功能。在整個周期中,ref 值是不變的
用法一:
// 例子
const Component = props => {
const [number, setNumber] = useState(0);
const inputRef = useRef(null)
const focus = useCallback(
() =>inputRef.focus(),
[]
);
return<div>
<input ref={inputRef}>
<button onClick={focus}>按鈕</button>
</div>;
};
用法二:類似於this
// 例子
const Component = props => {
const [number, setNumber] = useState(0);
const inputRef = useRef(null)
const focus = useCallback(
() =>inputRef.focus(),
[]
);
return<div>
<input ref={node => inputRef.current = node}>
<button onClick={focus}>按鈕</button>
</div>;
};
之所以能這麼用,在於applyRef這個函數,react也是類似。
export function applyRef(ref, value, vnode) {
try {
if (typeof ref == "function") ref(value);
else ref.current = value;
} catch (e) {
options._catchError(e, vnode);
}
}
查看useRef的源碼。
function useRef(initialValue) {
return useMemo(() => ({ current: initialValue }), []);
}
可見 就是初始化的時候創建一個{current:initialValue},不依賴任何數據,需要手動賦值修改
ReducerHookStateuseReduceruseReducer和使用redux非常像。
用法:
// reducer就是平時redux那種reducer函數
// initialState 初始化的state狀態
// init 一個函數用於惰性計算state初始值
const [state, dispatch] = useReducer(reducer, initialState, init);
計數器的例子。
const initialState = 0;
function reducer(state, action) {
switch (action.type) {
case "increment":
return { number: state.number + 1 };
case "decrement":
return { number: state.number - 1 };
default:
return state;
}
}
function init(initialState) {
return { number: initialState };
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState, init);
return (
<div>
{state.number}
<button onClick={() => dispatch({ type: "increment" })}>+</button>
<button onClick={() => dispatch({ type: "decrement" })}>-</button>
</div>
);
}
對於熟悉redux的同學來說,一眼明了。後面提到的useState舊是基於useReducer實現的。
源碼分析
export function useReducer(reducer, initialState, init) {
const hookState = getHookState(currentIndex++);
// 前面分析過ReducerHookState的數據結構,有兩個屬性
// _value 當前的state值
// _component 對應的組件實例
if (!hookState._component) {
// 初始化過程
// 因為後面需要用到setState更新,所以需要記錄component的引用
hookState._component = currentComponent;
hookState._value = [
// init是前面提到的惰性初始化函數,傳入了init則初始值是init的計算結果
// 沒傳init的時候是invokeOrReturn。這裡就是直接返回初始化值
/***
*
* ```js
* invokeOrReturn 很精髓
* 參數f為函數,返回 f(arg)
* 參數f非函數,返回f
* function invokeOrReturn(arg, f) {
return typeof f === "function" ? f(arg) : f;
}
* ```
*/
!init ? invokeOrReturn(undefined, initialState) : init(initialState),
action => {
// reducer函數計算出下次的state的值
const nextValue = reducer(hookState._value[0], action);
if (hookState._value[0] !== nextValue) {
hookState._value[0] = nextValue;
// setState開始進行下一輪更新
hookState._component.setState({});
}
}
];
}
// 返回當前的state
return hookState._value;
}
更新state就是調用 demo 的dispatch,也就是通過reducer(preState,action)計算出下次的state賦值給_value。然後調用組件的setState方法進行組件的diff和相應更新操作(這裡是preact和react不太一樣的一個地方,preact 的函數組件在內部和 class 組件一樣使用 component 實現的)。
useStateuseState大概是 hook 中最常用的了。類似於 class 組件中的 state 狀態值。
用法
const Component = () => {
const [number, setNumber] = useState(0);
const [index, setIndex] = useIndex(0);
return (
<div>
{/* setXxx可以傳入回調或者直接設置值**/}
<button onClick={() => setNumber(number => number + 1)}>
更新number
</button>
{number}
//
<button onClick={() => setIndex(index + 1)}>更新index</button>
{index}
</div>
);
};
上文已經提到過,useState是通過useReducer實現的。
export function useState(initialState) {
/***
*
* ```js
* function invokeOrReturn(arg, f) {
return typeof f === "function" ? f(arg) : f;
}
* ```
*/
return useReducer(invokeOrReturn, initialState);
}
只要我們給useReduecr的reducer參數傳invokeOrReturn函數即可實現useState。回顧下useState和useReducer的用法
const [index, setIndex] = useIndex(0);
setIndex(index => index + 1);
// or
setIndex(1);
//
const [state, dispatch] = useReducer(reducer, initialState);
dispatch({ type: "some type" });
對於setState直接傳值的情況。reducer(invokeOrReturn)函數,直接返回入參即可
// action非函數,reducer(hookState._value[0], action)結果為action
const nextValue = reducer(hookState._value[0], action);
對於setState直接參數的情況的情況。
// action為函數,reducer(hookState._value[0], action)結果為action(hookState._value[0])
const nextValue = reducer(hookState._value[0], action);
可見,useState其實只是傳特定reducer的useReducer一種實現。
EffectHookStateuseEffect 和 useLayoutEffect
這兩個 hook 的用法完全一致,都是在 render 過程中執行一些副作用的操作,可來實現以往 class 組件中一些生命周期的操作。區別在於,useEffect 的 callback 執行是在本次渲染結束之後,下次渲染之前執行。useLayoutEffect則是在本次會在瀏覽器 layout 之後,painting 之前執行,是同步的。
用法。傳遞一個回調函數和一個依賴數組,數組的依賴參數變化時,重新執行回調。
/**
* 接收一個包含一些必要副作用代碼的函數,這個函數需要從DOM中讀取layout和同步re-render
* `useLayoutEffect` 裡面的操作將在DOM變化之後,瀏覽器繪製之前 執行
* 儘量使用`useEffect`避免阻塞視圖更新
*
* @param effect Imperative function that can return a cleanup function
* @param inputs If present, effect will only activate if the values in the list change (using ===).
*/
export function useLayoutEffect(effect: EffectCallback, inputs?: Inputs): void;
/**
* 接收一個包含一些必要副作用代碼的函數。
* 副作用函數會在瀏覽器繪製後執行,不會阻塞渲染
*
* @param effect Imperative function that can return a cleanup function
* @param inputs If present, effect will only activate if the values in the list change (using ===).
*/
export function useEffect(effect: EffectCallback, inputs?: Inputs): void;
demofunction LayoutEffect() {
const [color, setColor] = useState("red");
useLayoutEffect(() => {
alert(color);
}, [color]);
useEffect(() => {
alert(color);
}, [color]);
return (
<>
<div id="myDiv" style={{ background: color }}>
顏色
</div>
<button onClick={() => setColor("red")}>紅</button>
<button onClick={() => setColor("yellow")}>黃</button>
<button onClick={() => setColor("blue")}>藍</button>
</>
);
}
從 demo 可以看出,每次改變顏色,useLayoutEffect的回調觸發時機是在頁面改變顏色之前,而useEffect的回調觸發時機是頁面改變顏色之後。它們的實現如下
export function useLayoutEffect(callback, args) {
const state = getHookState(currentIndex++);
if (argsChanged(state._args, args)) {
state._value = callback;
state._args = args;
currentComponent._renderCallbacks.push(state);
}
}
export function useEffect(callback, args) {
const state = getHookState(currentIndex++);
if (argsChanged(state._args, args)) {
state._value = callback;
state._args = args;
currentComponent.__hooks._pendingEffects.push(state);
}
}
它們的實現幾乎一模一樣,唯一的區別是useLayoutEffect的回調進的是renderCallbacks數組,而useEffect的回調進的是pendingEffects。
前面已經做過一些分析,_renderCallbacks是在_commit鉤子中執行的,在這裡執行上次renderCallbacks的effect的清理函數和執行本次的renderCallbacks。_commit則是在preact的commitRoot中被調用,即每次 render 後同步調用(顧名思義 renderCallback 就是 render 後的回調,此時 DOM 已經更新完,瀏覽器還沒有 paint 新一幀,上圖所示的 layout 後 paint 前)因此 demo 中我們在這裡alert會阻塞瀏覽器的 paint,這個時候看不到顏色的變化。
而_pendingEffects則是本次重繪之後,下次重繪之前執行。在 hook 中的調用關係如下
1、 options.differed 鉤子中(即組件 diff 完成後),執行afterPaint(afterPaintEffects.push(c))將含有_pendingEffects的組件推進全局的afterPaintEffects隊列
2、afterPaint中執行執行afterNextFrame(flushAfterPaintEffects)。在下一幀 重繪之前,執行flushAfterPaintEffects。同時,如果 100ms 內,當前幀的 requestAnimationFrame 沒有結束(例如窗口不可見的情況),則直接執行flushAfterPaintEffects。flushAfterPaintEffects函數執行隊列內所有組件的上一次的pendingEffects的清理函數和執行本次的pendingEffects。
幾個關鍵函數
/**
* 繪製之後執行回調
* 執行隊列內所有組件的上一次的`_pendingEffects`的清理函數和執行本次的`_pendingEffects`。
*/
function flushAfterPaintEffects() {
afterPaintEffects.some(component => {
if (component._parentDom) {
// 清理上一次的_pendingEffects
component.__hooks._pendingEffects.forEach(invokeCleanup);
// 執行當前_pendingEffects
component.__hooks._pendingEffects.forEach(invokeEffect);
component.__hooks._pendingEffects = [];
}
});
// 清空afterPaintEffects
afterPaintEffects = [];
}
/**
*preact的diff是同步的,是宏任務。
newQueueLength === 1 保證了afterPaint內的afterNextFrame(flushAfterPaintEffects)只執行一遍。因為會調用n次宏任務的afterPaint結束後,才會執行flushAfterPaintEffects一次將所有含有pendingEffect的組件進行回調進行
* */
afterPaint = newQueueLength => {
if (newQueueLength === 1 || prevRaf !== options.requestAnimationFrame) {
prevRaf = options.requestAnimationFrame;
// 執行下一幀結束後,清空 useEffect的回調
(prevRaf || afterNextFrame)(flushAfterPaintEffects);
}
};
/**
* 希望在下一幀 重繪之前,執行callback。同時,如果100ms內,當前幀的requestAnimationFrame沒有結束(例如窗口不可見的情況),則直接執行callback
*/
function afterNextFrame(callback) {
const done = () => {
clearTimeout(timeout);
cancelAnimationFrame(raf);
setTimeout(callback);
};
const timeout = setTimeout(done, RAF_TIMEOUT);
const raf = requestAnimationFrame(done);
}
useImperativeHandleuseImperativeHandle 可以讓你在使用 ref 時自定義暴露給父組件的實例值。在大多數情況下,應當避免使用 ref 這樣的命令式代碼。useImperativeHandle 應當與 forwardRef 一起
function FancyInput(props, ref) {
const inputRef = useRef();
// 第一個參數是 父組件 ref
// 第二個參數是返回,返回的對象會作為父組件 ref current 屬性的值
useImperativeHandle(ref, () => ({
focus: () => {
inputRef.current.focus();
}
}));
return <input ref={inputRef} ... />;
}
FancyInput = forwardRef(FancyInput);
function App(){
const ref = useRef()
return <div>
<FancyInput ref={ref}/>
<button onClick={()=>ref.focus()}>click</button>
</div>
}
默認情況下,函數組件是沒有ref屬性,通過forwardRef(FancyInput)後,父組件就可以往子函數組件傳遞ref屬性了。useImperativeHandle的作用就是控制父組件不能在拿到子組件的ref後為所欲為。如上,父組件拿到FancyInput後,只能執行focus,即子組件決定對外暴露的 ref 接口。
function useImperativeHandle(ref, createHandle, args) {
useLayoutEffect(
() => {
if (typeof ref === "function") ref(createHandle());
else if (ref) ref.current = createHandle();
},
args == null ? args : args.concat(ref)
);
}
useImperativeHandle的實現也是一目了然,因為這種是涉及到 dom 更新後的同步修改,所以自如是用useLayoutEffect實現的。從實現可看出,useImperativeHandle也能接收依賴項數組的
createContext接收一個 context 對象(Preact.createContext 的返回值)並返回該 context 的當前值。當前的 context 值由上層組件中距離當前組件最近的的 value prop 決定。當組件上層最近的 <MyContext.Provider> 更新時,該 Hook 會觸發重渲染,並使用最新傳遞給 MyContext provider 的 context value 值。
使用 context 最大的好處就是避免了深層組件嵌套時,需要一層層往下通過 props 傳值。使用 createContext 可以非常方便的使用 context 而不用再寫繁瑣的Consumer
const context = Preact.createContext(null);
const Component = () => {
// 每當Context.Provider value={{xx:xx}}變化時,Component都會重新渲染
const { xx } = useContext(context);
return <div></div>;
};
const App = () => {
return (
<Context.Provider value={{ xx: xx }}>
<Component></Component>
</Context.Provider>
);
};
useContext實現
function useContext(context) {
// 每個`preact`組件的context屬性都保存著當前全局context的Provider引用,不同的context都有一個唯一id
// 獲取當前組件 所屬的Context Provider
const provider = currentComponent.context[context._id];
if (!provider) return context._defaultValue;
const state = getHookState(currentIndex++);
if (state._value == null) {
// 初始化的時候將當前 組件訂閱 Provider的value變化
// 當Provider的value變化時,重新渲染當前組件
state._value = true;
provider.sub(currentComponent);
}
return provider.props.value;
}
可以看出,useContext會在初始化的時候,當前組件對應的Context.Provider會把該組件加入訂閱回調(provider.sub(currentComponent)),當 Provider value 變化時,在 Provider 的shouldComponentUpdate周期中執行組件的 render。
//
// Provider部分源碼
Provider(props) {
//....
// 初始化Provider的時候執行的部分
this.shouldComponentUpdate = _props => {
if (props.value !== _props.value) {
subs.some(c => {
c.context = _props.value;
// 執行sub訂閱回調組件的render
enqueueRender(c);
});
}
};
this.sub = c => {
subs.push(c);
let old = c.componentWillUnmount;
c.componentWillUnmount = () => {
// 組件卸載的時候,從訂閱回調組件列表中移除
subs.splice(subs.indexOf(c), 1);
old && old.call(c);
};
};
}
//....
總結:preact和react在源碼實現上有一定差異,但是通過對 preact hook 源碼的學習,對於理解 hook 的很多觀念和思想是非常有幫助的。
覺得本文對你有幫助?請分享給更多人
關注「前端大全」加星標,提升前端技能
好文章,我在看❤️