1. react hooks簡介
react hooks 是react 16.8.0 的新增特性吝岭,它可以讓你在不編寫class的情況下使用state以及其他的一些react特性。
??在過去的react版本中牙勘,如果我們想要使用狀態(tài)管理或者想要在render之后去做一些事情论泛,我們必須使用class組件才能辦到揩尸。但是現(xiàn)在hooks的出現(xiàn),使得函數(shù)組件也同樣可以做到屁奏。
??hooks實際上是一些以use開頭來明名的函數(shù)岩榆,它就像鉤子一樣,把函數(shù)組件不具備的特性鉤進來坟瓢,使得函數(shù)組件也同樣可以使用這些特性勇边。
??話不多說,下面我們就開始看一下第一個hook的api折联。
2.useState 使用規(guī)則
function User(props) {
let [count, setCount] = useState(0); // 這里的count粒褒,setCount類似于class組件里的state,setState诚镰,我們要改變count這個狀態(tài)的值奕坟,只需要調(diào)用setCount這個函數(shù)就可以了祥款,它接受一個參數(shù),就是你要更改的值月杉。
let [name, setName] = useState('Mary'); // 你可以在組件內(nèi)部多次調(diào)用useState來創(chuàng)建多個狀態(tài)變量
return <div>
<div>當(dāng)前計數(shù): count</div>
<button onClick={() => { setCount(count+1); }}>count+1</button>
</div>
}
??useState使得我們可以在函數(shù)組件里使用狀態(tài)刃跛,它接受一個參數(shù),就是當(dāng)前狀態(tài)的初始值苛萎。返回兩個變量奠伪,第一個變量就是我們的狀態(tài)變量,第二個就是改變這個狀態(tài)的函數(shù)首懈,類似于class組件里的state和setState绊率。
注意,這里useState返回的是一個數(shù)組究履,所以變量的名字是我們自己任意取的滤否。
useState | class state |
---|---|
可以在組件內(nèi)部多次使用,創(chuàng)建多個狀態(tài)變量 | 只有一個state對象 |
set函數(shù)最仑,傳進來的參數(shù)完全覆蓋該狀態(tài)值 | 合并state |
3.源碼分析
??當(dāng)前react版本為16.9.0藐俺。打開源碼字管,我們首先從react.js文件入手嗽测,找到useState的源碼家卖。
import {
useCallback,
useContext,
useEffect,
useImperativeHandle,
useDebugValue,
useLayoutEffect,
useMemo,
useReducer,
useRef,
useState, // 在這里
useResponder,
} from './ReactHooks'; // 所以我們要找的源碼在這個文件里面
??我們在進到ReactHooks.js文件里看一下
export function useState<S>(initialState: (() => S) | S) {
const dispatcher = resolveDispatcher();
return dispatcher.useState(initialState);
}
??從上述代碼可以看出默垄,我們的useState函數(shù)是掛到dispatcher對象上面的,那dispatcher對象到底是什么呢星虹,我們再進到resolveDispatcher函數(shù)里看一下挣饥。
??dispatcher對象被賦值為ReactCurrentDispatcher.current自晰,我們在進一步看一下ReactCurrentDispatcher是什么剑逃。
import type {Dispatcher} from 'react-reconciler/src/ReactFiberHooks';
const ReactCurrentDispatcher = {
/**
* @internal
* @type {ReactComponent}
*/
current: (null: null | Dispatcher), // current是Dispatcher類型的
};
export default ReactCurrentDispatcher;
??dispatcher我們看到ReactCurrentDispatcher.current被初始化為null浙宜,似乎到這里我們什么也沒找到。
??但我們找到了一條線索蛹磺,那就是useState其實是掛載ReactCurrentDispatcher.current對象上面的粟瞬,所以我們只要找到它被賦值的地方就可以了。
??但這部分的內(nèi)容萤捆,實際上屬于fiber調(diào)度的范疇裙品,所以我們就簡單提一下,不做過多闡述俗或,實際上真正賦值的地方是在render階段.
ReactCurrentDispatcher.current =
nextCurrentHook === null
? HooksDispatcherOnMount // 組件掛載階段
: HooksDispatcherOnUpdate; // 組件更新階段
??上面代碼,當(dāng)nextCurrentHook為空的時候蕴侣,被賦值為HooksDispatcherOnMount焰轻,不為空的時候被賦值為HooksDispatcherOnUpdate,意思就是說昆雀,當(dāng)組件第一次render辱志,也就是掛載的時候,我們的hook api是在HooksDispatcherOnMount這個對象上的狞膘,非首次渲染是在HooksDispatcherOnUpdate對象上的揩懒。
const HooksDispatcherOnMount: Dispatcher = {
readContext,
useCallback: mountCallback,
useContext: readContext,
useEffect: mountEffect,
...
useState: mountState,
...
};
const HooksDispatcherOnUpdate: Dispatcher = {
readContext,
useCallback: updateCallback,
useContext: readContext,
useEffect: updateEffect,
...
useState: updateState,
...
};
??所以我們需要分兩個分支來看源碼。
3.1 mountState
??首先我們需要知道挽封,在組件里已球,多次調(diào)用useState,或者其他hook辅愿,那react怎么知道我們當(dāng)前是哪一個hook呢智亮。其實在react內(nèi)部,所有的hook api第一次被調(diào)用的時候都會先創(chuàng)建一個hook對象点待,來保存相應(yīng)的hook信息阔蛉。然后,這個hook對象癞埠,會被加到一個鏈表上状原,這樣我們每次渲染的時候,只要從這個鏈表上面依次的去取hook對象苗踪,就知道了當(dāng)前是哪一個hook了颠区。
下面我們就看一下這個hook對象的具體格式。
const hook: Hook = {
memoizedState: null, // 緩存當(dāng)前state的值
baseState: null, // 初始化initState通铲,以及每次dispatch之后的newState
queue: null, // update quene
baseUpdate: null, //基于哪一個hook進行更新毕莱,循環(huán)update quene的起點
next: null, // 指向下一個hook
};
對于useState來說,memoizedState屬性上保存的就是當(dāng)前hook對應(yīng)狀態(tài)變量當(dāng)前的值颅夺,也就是我們獲取到的狀態(tài)變量的值央串。那這個quene上面保存的是什么呢,我們稍后在解釋碗啄。
??言歸正傳质和,我們開始將mountState函數(shù)。組件首次渲染的源碼稚字,就是mountState這個函數(shù)饲宿。也就是說首次渲染時useState的源碼就是mountState。
那么我們來看看它的實現(xiàn)胆描。
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const hook = mountWorkInProgressHook(); // 第一步:創(chuàng)建新的hook對象并加到鏈上瘫想,返回workInProgressHook
if (typeof initialState === 'function') {
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState; // 第二步:獲取初始值并初始化hook對象
const queue = (hook.queue = { // 第三步:創(chuàng)建更新隊列(update quene),并初始化
last: null, // 最后一次的update對象
dispatch: null, // 更新函數(shù)
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any), // 前面最后一次更新的state值昌讲,更新的值有可能是函數(shù)国夜,函數(shù)計算需要用到前一個state的值
});
const dispatch: Dispatch<
BasicStateAction<S>,
> = (queue.dispatch = (dispatchAction.bind( // 第四步
null,
// Flow doesn't know this is non-null, but we do.
((currentlyRenderingFiber: any): Fiber), // 綁定當(dāng)前fiber和quene
queue,
): any));
return [hook.memoizedState, dispatch];
}
第一步,創(chuàng)建hook對象短绸,并將該hook對象加到hook鏈的末尾车吹。
我們來看一下代碼筹裕。
function mountWorkInProgressHook(): Hook {
const hook: Hook = { // 創(chuàng)建hook對象
memoizedState: null,
baseState: null,
queue: null,
baseUpdate: null,
next: null,
};
if (workInProgressHook === null) { // 如果是組件內(nèi)部的第一個hook
// This is the first hook in the list
firstWorkInProgressHook = workInProgressHook = hook;
} else { // 不是第一個hook對象,就直接把新創(chuàng)建的hook對象加到hook鏈表的末尾
// Append to the end of the list
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
第二步:初始化hook對象的狀態(tài)值窄驹,也就是我們傳進來的initState的值朝卒。
第三步:創(chuàng)建更新隊列,這個隊列是更新狀態(tài)值的時候用的乐埠。
第四步:綁定dispatchAction函數(shù)抗斤。我們可以看到最后一行返回的就是這個函數(shù)。也就是說這個函數(shù)丈咐,其實就是我們改變狀態(tài)用的函數(shù)瑞眼,就相當(dāng)于是setState函數(shù)。這里它先做了一個綁定當(dāng)前quene和fiber對象的動作棵逊,就是為了在調(diào)用setState的時候伤疙,知道該更改的是那一個狀態(tài)的值。
??至此歹河,我們就看完了mountState函數(shù)掩浙。
下面這張圖,是我自己畫的簡易版useState源碼的流程圖秸歧。
??那么到這里厨姚,我們已經(jīng)走完了組件首次渲染調(diào)用useState時的邏輯。現(xiàn)在键菱,我們已經(jīng)拿到了我們的狀態(tài)變量state谬墙,那么我們就可以改變這個狀態(tài)了,也就是調(diào)用set函數(shù)经备,這里為了說明方便拭抬,我們就直接說setState函數(shù)了(實際上你可以隨意取名字)。
3.2 dispatchAction
前面已經(jīng)說過dispatchAction就是我們更改狀態(tài)值時調(diào)用的函數(shù)侵蒙。
function dispatchAction<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A, // 2
) {
...
if(){
rerender邏輯
}else{
const update: Update<S, A> = { // 第一步
expirationTime,
suspenseConfig,
action, // 2
eagerReducer: null,
eagerState: null,
next: null,
};
// 第二步:將update加到quene上,更新quene的last為當(dāng)前update,注意quene是一個環(huán)形鏈表
const last = queue.last;
if (last === null) {
// This is the first update. Create a circular list.
update.next = update; // 環(huán)形鏈
} else {
const first = last.next; // 這個last.next是指向第一個update造虎,因為quene是一個環(huán)形鏈表
if (first !== null) {
// Still circular.
update.next = first; // 使quene變成環(huán)形鏈表
}
last.next = update; // 將update加到quene上。
}
queue.last = update; // 更新quene的last為當(dāng)前update
}
...
省略無關(guān)代碼纷闺,我們可以看到實際上算凿,dispatchAction這個函數(shù)主要做了兩件事情。
第一件就是創(chuàng)建了一個update對象犁功,這個對象上面保存了本次更新的相關(guān)信息氓轰,包括新的狀態(tài)值action。
第二件浸卦,就是將所有的update對象串成了一個環(huán)形鏈表署鸡,保存在我們hook對象的queue屬性上面。所以我們就知道了queue這個屬性的意義,它是保存所有更新行為的地方靴庆。
在這里我們可以看到时捌,我們要更改的狀態(tài)值并沒有真的改變,只是被緩存起來了撒穷。那么真正改變狀態(tài)值的地方在哪呢匣椰?答案就是在下一次render時裆熙,函數(shù)組件里的useState又一次被調(diào)用了端礼,這個時候才是真的更新state的時機。
3.3 updateState
這里就是我們組件更新時入录,調(diào)用useState時真正走的邏輯了蛤奥。
function updateState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
return updateReducer(basicStateReducer, (initialState: any));
}
function updateReducer<S, I, A>(
reducer: (S, A) => S, // 對于useState來說就是basicStateReducer
initialArg: I,
init?: I => S,
): [S, Dispatch<A>] {
const hook = updateWorkInProgressHook(); // 獲取當(dāng)前正在工作的hook,Q1
const queue = hook.queue; // 更新隊列
// The last update in the entire queue
const last = queue.last; // 最后一次的update對象
// The last update that is part of the base state.
const baseUpdate = hook.baseUpdate; // 上一輪更新的最后一次更新對象
const baseState = hook.baseState; // 上一次的action僚稿,現(xiàn)在是初始值
// Find the first unprocessed update.
let first;
if (baseUpdate !== null) {
if (last !== null) {
// For the first update, the queue is a circular linked list where
// `queue.last.next = queue.first`. Once the first update commits, and
// the `baseUpdate` is no longer empty, we can unravel the list.
last.next = null; // 因為quene是一個環(huán)形鏈表凡桥,所以這里要置空
}
first = baseUpdate.next; // 第一次是用的last.next作為第一個需要更新的update,第二次之后就是基于上一次的baseUpdate來開始了(baseUpdate就是上一次的最后一個更新)
} else {
first = last !== null ? last.next : null; // last.next是第一個update
}
if (first !== null) { // 沒有更新,則不需要執(zhí)行蚀同,直接返回
let newState = baseState;
let newBaseState = null;
let newBaseUpdate = null;
let prevUpdate = baseUpdate;
let update = first;
let didSkip = false;
do { // 循環(huán)鏈表缅刽,執(zhí)行每一次更新
const updateExpirationTime = update.expirationTime;
if (updateExpirationTime < renderExpirationTime) {
// Priority is insufficient. Skip this update. If this is the first
// skipped update, the previous update/state is the new base
...
} else { // 正常邏輯
// This update does have sufficient priority.
// Process this update.
if (update.eagerReducer === reducer) { // 如果是useState,他的reducer就是basicStateReducer
// If this update was processed eagerly, and its reducer matches the
// current reducer, we can use the eagerly computed state.
newState = ((update.eagerState: any): S);
} else {
const action = update.action;
newState = reducer(newState, action);
}
}
prevUpdate = update;
update = update.next;
} while (update !== null && update !== first);
if (!didSkip) { // 不跳過蠢络,就更新baseUpdate和baseState
newBaseUpdate = prevUpdate;
newBaseState = newState;
}
...
hook.memoizedState = newState; // 更新hook對象
hook.baseUpdate = newBaseUpdate;
hook.baseState = newBaseState;
queue.lastRenderedState = newState;
}
const dispatch: Dispatch<A> = (queue.dispatch: any);
return [hook.memoizedState, dispatch];
}
updateState做的事情衰猛,實際上就是拿到更新隊列,循環(huán)隊列刹孔,并根據(jù)每一個update對象對當(dāng)前hook進行狀態(tài)更新啡省。最后返回最終的結(jié)果。
這是我在學(xué)習(xí)useState源碼時的自問自答
1髓霞、怎么循環(huán)hook對象的卦睹,在哪里操作的
(1)從當(dāng)前fiber對象的memoizedState屬性保存著當(dāng)前組件的第一個hook對象
(2)在每次執(zhí)行updateState的時候,首先需要獲取當(dāng)前工作中的hook方库,就是在這里循環(huán)的hook
(3)hook鏈?zhǔn)且粋€環(huán)形鏈嗎结序?不是,是單向鏈表
在mount階段纵潦,workInProgressHook.next = null,update階段最后一個hook的next依然是null
是不是說當(dāng)前fiber對象的memoizedState一直都是第一個hook (462行)
2.Q:更新函數(shù)綁定當(dāng)前hook的地方在哪
A:在dispatchAcion.bind的地方徐鹤,綁定了fiber和quene
3.Q:更新state時,怎么定位到第一個需要執(zhí)行的update的
A:基于baseUpdate來開始更新
4.Q:renderWithHooks為什么第一次沒有執(zhí)行 FunctionComponent這個分支酪穿?
A:renderWithHooks是在組件更新階段執(zhí)行的FunctionComponent
5.Q:useState可以放對象嗎凳干?
A:可以,但是如果setState里的對象還是同一個就不會觸發(fā)重新渲染
第一次正式的寫技術(shù)文章被济,作文水平有限救赐,希望可以幫到大家。
參考
[掘金]? useState源碼解析