來來來,手摸手寫一個hook
hello,這里是瀟晨,今天就帶著大家一起來手寫一個迷你版的hooks,方便大家理解hook在源碼中的運行機制,配有圖解,保姆級的教程,只求同學(xué)一個小小的??,??。
第一步:引入React和ReactDOM
因為我們要將jsx轉(zhuǎn)變?yōu)?code>virtual-dom,這一步分工作就交給babel吧,而jsx被babel進行詞法解析之后會形成React.createElement()的調(diào)用,而React.createElement()執(zhí)行之后的返回結(jié)果就是jsx對象或者叫virtual-dom。
又因為我們要將我們的demo渲染到dom上,所以我們引入ReactDOM。
import React from "react";
import ReactDOM from "react-dom";
第二步:我們來寫一個小demo
我們定義兩個狀態(tài)count和age,在點擊的時候觸發(fā)更新,讓它們的值加1。
在源碼中useState是保存在一個Dispatcher對象上面的,并且在mount和update的時候取到的是不同的hooks,所以我們先暫時從Dispatcher上拿到useState,等下在來定義Dispatcher。
接下來定義一個schedule函數(shù),每次調(diào)用的時候會重新渲染組件。
function App() {
let [count, setCount] = Dispatcher.useState(1);
let [age, setAge] = Dispatcher.useState(10);
return (
<>
<p>Clicked {count} times</p>
<button onClick={() => setCount(() => count + 1)}> Add count</button>
<p>Age is {age}</p>
<button onClick={() => setAge(() => age + 1)}> Add age</button>
</>
);
}
function schedule() { //每次調(diào)用會重新渲染組件
ReactDOM.render(<App />, document.querySelector("#root"));
}
schedule();
第三步:定義Dispatcher
在看這部分前,先來捋清楚fiber、hook、update的關(guān)系,看圖:

Dispatcher是什么:Dispatcher在源碼中就是一個對象,上面存放著各種各樣的hooks,在mount和update的時候會使用過不同的Dispatcher,來看看在源碼中Dispatcher是什么樣子:
在調(diào)用useState之后,會調(diào)用一個resolveDispatcher的函數(shù),這個函數(shù)調(diào)用之后會返回一個dispatcher對象,這個對象上就有useState等鉤子。

那我們來看看這個函數(shù)做了啥事情,這個函數(shù)比較簡單,直接從ReactCurrentDispatcher對象上拿到current,然后返回出來的這個current就是dispatcher,那這個ReactCurrentDispatcher又是個啥?別急,繼續(xù)在源碼中來找一下。

在源碼中有這樣一段代碼,如果是在正式環(huán)境中,分為兩種情況
- 如果滿足
current === null || current.memoizedState === null,說明我們處于首次渲染的時候,也就是mount的時候,其中current就是我們fiber節(jié)點,memoizedState保存了fiber上hook,也就是說在應(yīng)用首次渲染的時候,current fiber是不存在的,我們還沒有創(chuàng)造出任何fiber節(jié)點,或者存在某些fiber,但是上面沒有構(gòu)建相應(yīng)的hook,這個時候就可以認為是處于首次渲染的時候,我們?nèi)〉降氖?code>HooksDispatcherOnMount - 如果不滿足
current === null || current.memoizedState === null,就說明我們處于更新階段,也就是update的時候,我們?nèi)〉降氖?code>HooksDispatcherOnUpdate
if (__DEV__) {
if (current !== null && current.memoizedState !== null) {
ReactCurrentDispatcher.current = HooksDispatcherOnUpdateInDEV;
} else if (hookTypesDev !== null) {
ReactCurrentDispatcher.current = HooksDispatcherOnMountWithHookTypesInDEV;
} else {
ReactCurrentDispatcher.current = HooksDispatcherOnMountInDEV;
}
} else {
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
}
那我們就來看一下這個HooksDispatcherOnMount和HooksDispatcherOnUpdate是個什么,好家伙,原來你包含了所有的hooks啊。
const HooksDispatcherOnMount: Dispatcher = {
readContext,
useCallback: mountCallback,
useContext: readContext,
useEffect: mountEffect,
useImperativeHandle: mountImperativeHandle,
useLayoutEffect: mountLayoutEffect,
useMemo: mountMemo,
useReducer: mountReducer,
useRef: mountRef,
useState: mountState,
useDebugValue: mountDebugValue,
useDeferredValue: mountDeferredValue,
useTransition: mountTransition,
useMutableSource: mountMutableSource,
useOpaqueIdentifier: mountOpaqueIdentifier,
unstable_isNewReconciler: enableNewReconciler,
};
const HooksDispatcherOnUpdate: Dispatcher = {
readContext,
useCallback: updateCallback,
useContext: readContext,
useEffect: updateEffect,
useImperativeHandle: updateImperativeHandle,
useLayoutEffect: updateLayoutEffect,
useMemo: updateMemo,
useReducer: updateReducer,
useRef: updateRef,
useState: updateState,
useDebugValue: updateDebugValue,
useDeferredValue: updateDeferredValue,
useTransition: updateTransition,
useMutableSource: updateMutableSource,
useOpaqueIdentifier: updateOpaqueIdentifier,
unstable_isNewReconciler: enableNewReconciler,
};
所以dispatcher就是個對象,里面包含了所有的hooks,在首次渲染和更新的時候拿到的是不同的dispatcher,在調(diào)用hooks的時候就會調(diào)用到不同的函數(shù),比如如果使用了useState,在mount的時候調(diào)用到的就是mountState,在update的時候調(diào)用到的就是updateState。

現(xiàn)在我們來手寫一下dispatcher,dispatcher是個對象,對象上存在useState,我們用一個自執(zhí)行函數(shù)來表示,此外還需要用到兩個變量和一個常量fiber
-
workInProgressHook表示遍歷到的hook(因為hook會保存在鏈表上,需要遍歷鏈表計算hook上保存的狀態(tài)) - 為了簡單起見,定義一個
isMount=true表示mount的時候,在update的時候?qū)⑺O(shè)置成false, - 為簡單起見,
fiber就定義成一個對象,memoizedState表示這個fiber節(jié)點上存放的hook鏈表,stateNode就是第二步的demo。
let workInProgressHook;//當前工作中的hook
let isMount = true;//是否時mount時
const fiber = {//fiber節(jié)點
memoizedState: null,//hook鏈表
stateNode: App
};
const Dispatcher = (() => {//Dispatcher對象
function useState(){
//。。。
}
return {
useState
};
})();
在定義useState之前,首先來看看hook和update的數(shù)據(jù)結(jié)構(gòu)
hook:
-
queue:上面有pending屬性,pending也是一條環(huán)狀鏈表,上面存放了未被更新的update,也就是說這些update會以next指針連接成環(huán)狀鏈表。 -
memoizedState表示當前的狀態(tài) -
next:指向下一個hook,形成一條鏈表
const hook = {//構(gòu)建hook
queue: {
pending: null//未執(zhí)行的update鏈表
},
memoizedState: null,//當前state
next: null//下一個hook
};
update:
-
action:是出發(fā)更新的函數(shù) -
next:連接下一個update,形成一條環(huán)狀鏈表
const update = {//構(gòu)建update
action,
next: null
};
那接下來定義useState吧,分三個部分:
- 創(chuàng)建
hook或取到hook:- 在
mount的時候:調(diào)用mountWorkInProgressHook創(chuàng)建一個初始的hook,賦值useState傳進來的初始值initialState - 在
update的時候:調(diào)用updateWorkInProgressHook,拿到當前正在工作的hook
- 在
- 計算
hook上未更新的狀態(tài):遍歷hook上的pending鏈表,調(diào)用鏈表節(jié)點上的action函數(shù),生成一個新的狀態(tài),然后更新hook上的狀態(tài)。 - 返回新的狀態(tài)和
dispatchAction傳入queue參數(shù)
function useState(initialState) {
//第1步:創(chuàng)建hook或取到hook
let hook;
if (isMount) {
hook = mountWorkInProgressHook();
hook.memoizedState = initialState;//初始狀態(tài)
} else {
hook = updateWorkInProgressHook();
}
//第2步:計算hook上未更新的狀態(tài)
let baseState = hook.memoizedState;//初始狀態(tài)
if (hook.queue.pending) {
let firstUpdate = hook.queue.pending.next;//第一個update
do {
const action = firstUpdate.action;
baseState = action(baseState);//調(diào)用action計算新的狀態(tài)
firstUpdate = firstUpdate.next;//通過update的action計算state
} while (firstUpdate !== hook.queue.pending);//當鏈表還沒遍歷完時 進行循環(huán)
hook.queue.pending = null;//重置update鏈表
}
hook.memoizedState = baseState;//賦值新的state
//第3步:返回新的狀態(tài)和dispatchAction傳入queue參數(shù)
return [baseState, dispatchAction.bind(null, hook.queue)];//useState的返回
}
接下來定義mountWorkInProgressHook和updateWorkInProgressHook這兩個函數(shù)
-
mountWorkInProgressHook:在mount的時候調(diào)用,新創(chuàng)建一個hook對象,- 如果當前
fiber不存在memoizedState,那當前hook就是這個fiber上的第一個hook,將hook賦值給fiber.memoizedState - 如果當前
fiber存在memoizedState,那將當前hook接在workInProgressHook.next后面。 - 將當前
hook賦值給workInProgressHook
- 如果當前
-
updateWorkInProgressHook:在update的時候調(diào)用,返回當前的hook,也就是workInProgressHook,并且將workInProgressHook指向hook鏈表的下一個。
function mountWorkInProgressHook() {//mount時調(diào)用
const hook = {//構(gòu)建hook
queue: {
pending: null//未執(zhí)行的update鏈表
},
memoizedState: null,//當前state
next: null//下一個hook
};
if (!fiber.memoizedState) {
fiber.memoizedState = hook;//第一個hook的話直接賦值給fiber.memoizedState
} else {
workInProgressHook.next = hook;//不是第一個的話就加在上一個hook的后面,形成鏈表
}
workInProgressHook = hook;//記錄當前工作的hook
return workInProgressHook;
}
function updateWorkInProgressHook() {//update時調(diào)用
let curHook = workInProgressHook;
workInProgressHook = workInProgressHook.next;//下一個hook
return curHook;
}
第四步:定義dispatchAction
-
創(chuàng)建
update,掛載載queue.pending上- 如果之前
queue.pending不存在,那創(chuàng)建的這個update就是第一個,則update.next = update - 如果之前
queue.pending存在,則將創(chuàng)建的這個update加入queue.pending的環(huán)狀鏈表中
1 - 如果之前
讓
isMount=false,并且賦值workInProgressHook,調(diào)用schedule進行更新渲染
function dispatchAction(queue, action) {//觸發(fā)更新
const update = {//構(gòu)建update
action,
next: null
};
if (queue.pending === null) {
update.next = update;//update的環(huán)狀鏈表
} else {
update.next = queue.pending.next;//新的update的next指向前一個update
queue.pending.next = update;//前一個update的next指向新的update
}
queue.pending = update;//更新queue.pending
isMount = false;//標志mount結(jié)束
workInProgressHook = fiber.memoizedState;//更新workInProgressHook
schedule();//調(diào)度更新
}
最終代碼
import React from "react";
import ReactDOM from "react-dom";
let workInProgressHook;//當前工作中的hook
let isMount = true;//是否時mount時
const fiber = {//fiber節(jié)點
memoizedState: null,//hook鏈表
stateNode: App//dom
};
const Dispatcher = (() => {//Dispatcher對象
function mountWorkInProgressHook() {//mount時調(diào)用
const hook = {//構(gòu)建hook
queue: {
pending: null//未執(zhí)行的update鏈表
},
memoizedState: null,//當前state
next: null//下一個hook
};
if (!fiber.memoizedState) {
fiber.memoizedState = hook;//第一個hook的話直接賦值給fiber.memoizedState
} else {
workInProgressHook.next = hook;//不是第一個的話就加在上一個hook的后面,形成鏈表
}
workInProgressHook = hook;//記錄當前工作的hook
return workInProgressHook;
}
function updateWorkInProgressHook() {//update時調(diào)用
let curHook = workInProgressHook;
workInProgressHook = workInProgressHook.next;//下一個hook
return curHook;
}
function useState(initialState) {
let hook;
if (isMount) {
hook = mountWorkInProgressHook();
hook.memoizedState = initialState;//初始狀態(tài)
} else {
hook = updateWorkInProgressHook();
}
let baseState = hook.memoizedState;//初始狀態(tài)
if (hook.queue.pending) {
let firstUpdate = hook.queue.pending.next;//第一個update
do {
const action = firstUpdate.action;
baseState = action(baseState);
firstUpdate = firstUpdate.next;//循環(huán)update鏈表
} while (firstUpdate !== hook.queue.pending);//通過update的action計算state
hook.queue.pending = null;//重置update鏈表
}
hook.memoizedState = baseState;//賦值新的state
return [baseState, dispatchAction.bind(null, hook.queue)];//useState的返回
}
return {
useState
};
})();
function dispatchAction(queue, action) {//觸發(fā)更新
const update = {//構(gòu)建update
action,
next: null
};
if (queue.pending === null) {
update.next = update;//update的環(huán)狀鏈表
} else {
update.next = queue.pending.next;//新的update的next指向前一個update
queue.pending.next = update;//前一個update的next指向新的update
}
queue.pending = update;//更新queue.pending
isMount = false;//標志mount結(jié)束
workInProgressHook = fiber.memoizedState;//更新workInProgressHook
schedule();//調(diào)度更新
}
function App() {
let [count, setCount] = Dispatcher.useState(1);
let [age, setAge] = Dispatcher.useState(10);
return (
<>
<p>Clicked {count} times</p>
<button onClick={() => setCount(() => count + 1)}> Add count</button>
<p>Age is {age}</p>
<button onClick={() => setAge(() => age + 1)}> Add age</button>
</>
);
}
function schedule() {
ReactDOM.render(<App />, document.querySelector("#root"));
}
schedule();
預(yù)覽效果:https://codesandbox.io/s/custom-hook-tyf19?file=/src/index.js
