詳細 preact hook 源碼逐行解析

2021-03-02 前端大全

(給前端大全加星標,提升前端技能)

作者: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 這個比較特殊

MemoHookState

MemoHook是一類用來和性能優化有關的 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},不依賴任何數據,需要手動賦值修改

ReducerHookStateuseReducer

useReducer和使用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 實現的)。

useState

useState大概是 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一種實現。

EffectHookState

useEffect 和 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;

demo

function 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);

}

useImperativeHandle

useImperativeHandle 可以讓你在使用 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 的很多觀念和思想是非常有幫助的。

覺得本文對你有幫助?請分享給更多人

關注「前端大全」加星標,提升前端技能

好文章,我在看❤️

相關焦點

  • Java8線程池ThreadPoolExecutor底層原理及其源碼解析
    , 並且workQueue已經滿了, 但是workers.size()<maximumPoolSize, 就創建一個臨時線程處理task.當workers.size()大於corePoolSize時, 並且workQueue已經滿了, 但是workers.size()>=maximumPoolSize, 執行拒絕策略.後續會有對ThreadPoolExecutor#execute方法的詳細解讀
  • Android免Root權限Hook系統函數修改程序運行時內存指令邏輯
    dex文件的格式,不了解的同學可以點擊看這篇文章:Android中Dex文件格式解析,然後使用系統函數修改內存讀寫屬性,在通過指定方法名找到該方法在內存的指令地址,然後替換即可。這個不難,因為我們想修改程序運行時態指令,那麼肯定和dex加載解析過程分不開,這個就簡單了,直接去 [Android源碼/dalvik/libdex/] 下找到DexFile.h頭文件,查看他的內部函數聲明和一些數據結構定義信息:
  • 基於hook和gmock開展單元測試
    中的一個target,該target和之前BVT的target的區別在於,其是基於MAC OSX的Command Line工程,運行環境是MAC OSX,類似於Windows下的可執行文件,而BVT自動化的case運行環境都是基於iOS或者是iOS Simulator系統,這些差別所帶來的影響會在第4節中詳細說明。
  • Jetpack源碼解析--ViewModel基本使用及源碼解析
    1.背景Jetpack源碼解析系列文章:1. Android_Jetpack組件---Naviagtion源碼解析2. Jetpack源碼解析—Navigation為什麼切換Fragment會重繪?3. Jetpack源碼解析---用Lifecycles管理生命周期4.
  • 抖音解析網站源碼 抖音解析保存在哪裡
    抖音解析網站源碼有沒有呢,這是很多有這方面需求的小夥伴們都關心的問題。就讓小編帶大家了解抖音解析保存在哪裡吧~
  • React Hook 入門教程
    react hook 產生的原因在組件之間復用狀態邏輯很難組件之間復用狀態邏輯很困難,我們可能會把組件連結到我們的 store 裡面。結合上文 hook 產生的原因,react hook 就是讓你多寫函數組件,當你需要使用到一些狀態管理、副作用方法的時候,可以「鉤」進來。你可以使用 react 自帶的 hook,也可以自己寫 hook。
  • 技術乾貨:Python解析CSV
    聽起來好像很容易解析CSV文件,是嗎? 可能不假所思地認只須調用str.split(",")即可. 但不能夠這樣做,因為有些欄位可能含有嵌套的逗號,因此需要專門用於解析和生成CSV的庫,如Python的csv庫.3. 案例通過將源碼時代的學科數據,以CSV格式輸出到文件中,接著將同樣的數據讀出來顯示在控制臺上。4.
  • 【Hook】實現無清單啟動Activity的應用
    三.Demo源碼講解宿主所以,用hook技術實現插件化啟動Activity,完整思路是:{ public static void hook(Context context) { hookAMS(context);//使用假的Activity,騙過AMS的檢測 if (ifSdkOverIncluding28()) hookActivityThread_mH_AfterIncluding28
  • 如何安全的Post-Hook一個函數
    使用安全的方式  local orig_foo = foo  function posthook(a1, ...)  另一個好處是, 我們使用了局部變量來保存原始方法並做了一個適當的尾調用可以帶來更好的性能, 從而為我們的hook做了最小化的付出.  會帶來巨大的性能影響麼?  在WoW-2.0以前的設計中, 使用unpack(), 在每次hook被調用時創建一個垃圾回收表. 在新的設計中改進了, 使用'...'變量, 去掉了垃圾回收這部分源碼.
  • Spring-Task源碼解析
    this.registerBeanDefinitionParser("scheduler",newSchedulerBeanDefinitionParser());}schedulerSchedulerBeanDefinitionParser源碼
  • DBLE 網絡模塊源碼解析之網絡 IO 基礎知識-愛可生
    研讀 DBLE 網絡模塊的源碼,能夠讓你對網絡 IO 的處理有更進一步的理解。為什麼連接 DBLE 能夠像連接 MySQL 一樣?為什麼 DBLE 的性能能夠如此高?希望通過本系列文章,能夠幫助大家對DBLE的網絡模塊有更深入的了解,更進一步,希望能夠幫助大家對高性能網絡 IO 有更深入的了解。
  • 內存解析Il2cpp函數地址
    相對u3d遊戲進行hook基本上離不開 Il2CppDumper 的使用,有時候想要找個函數什麼的就很麻煩,第一步就得使用上面這工具來找到函數地址。2. 第二就是每次都要用 Il2CppDumper 來找函數地址不能方便的實現 frida 腳本的自動化,既然都用到了 frida 當然是越方便越爽。
  • 強大的反射功能詳解與應用源碼解析
    (以上均參考自《通用源碼閱讀指導書——MyBatis源碼詳解》)以上例子的來源於《通用源碼閱讀指導書——MyBatis源碼詳解》中的擴展閱讀部分提及的開源項目ObjectLogger( https://github.com/yeecode/ObjectLogger )。我們這裡只是ObjectLogger的一個非常簡化的實現。
  • 深入淺出解析React Router 源碼
    這是由於 pushState 的 url 必須與當前的 url 同源,而 file:// 形式打開的頁面沒有 origin ,導致報錯。如果想正常運行體驗,可以使用http-server為文件啟動一個本地服務。
  • Redux-saga Effect方法功能以及源碼解析
    call方法源碼及功能解析直接去effectRunnerMap.js看runCallEffect方法runCallEffect方法功能及源碼解析 function runCallEffect(env, { context, fn, args }, cb, { task }) { // catch synchronous failures; see #152
  • React 16.8 之 React Hook
    仔細看第一行我們引入了useState這個hook,就是這個hook讓我們的無狀態組件成為了一個有狀態的組件。State Hook 解決存儲持久化的方案我所知道的可以通過js存儲持久化狀態的方法有: class類、全局變量、DOM、閉包通過學習react hook源碼解析了解到是用閉包實現的~function useState(initialState
  • 阿里P8架構師力薦的 Java源碼解析及面試合集
    源碼解析和設計思路06 LinkedList 源碼解析07 List 源碼會問哪些面試題08 HasMap源碼解析>09 [x]TreeMap 和 LinkedHashMap核心源碼解析10 Map源碼會問哪些面i試題11 [X]HashSet 、TreeSet 源碼解析
  • Hive 不為人知 但 到處都是Hook!
    今天看hive源碼的時候看到了hook, 之前從沒聽過,特意學習了一下。而且看源碼的時候發現hook 到處都是,每個類都有點那種。比如: HiveDriverRunHookContext hookContext = new HiveDriverRunHookContextImpl(conf, alreadyCompiled ?
  • pytest hook - 我的用例我做主
    pytest除了強大的用例發現、收集、執行和報告輸出之外,還提供了豐富的函數hook可以自定義插件或者框架行為,只需在項目目錄/conftest.py文件中實現對應hook名稱的函數,即可完成hook函數的註冊。