React 的 logo 是一個原子圖案, 原子組成了物質的表現。相似的, React 就像原子般構成了頁面的表現; 而 Hooks 就如夸克, 更接近 React 的本質, 可是直到 4 年後的今天才被設計出來。 —— Dan in React Conf(2018)html
React在18年推出了hooks這一思想,以一種新的思惟模式去構建web App。咱們都知道,React認爲,UI視圖是數據的一種視覺映射,即UI = F(data)
,F須要負責對輸入數據進行加工、並對數據的變動作出響應。 React給UI的複用提供了極大的便利,可是對於邏輯的複用,在Class Component中並非那麼方便。在有Hooks以前,邏輯的複用一般是使用HOC來實現,使用HOC會帶來一些問題:前端
在有Hooks以前,Function Component只能單純地接收props、事件,而後返回jsx,自己是無狀態的組件,依賴props來響應數據(狀態)的變動,而上面提到的依賴都是從Class Component傳入的,因此在有Hooks以前Function Component是要徹底依賴Class Component存在的。可是這上面這些在Hooks出現以後所有都被打破。 本文不會介紹hooks的使用方式,詳見 官方文檔。react
在使用了Hooks以後,你確定會對Hooks的原理很是感興趣,本文要講述的就是React Hooks的「黑魔法」,主要內容有:web
一個Hook結構以下:數組
export type Hook = {
memoizedState: any,
baseState: any,
baseUpdate: Update<any, any> | null,
queue: UpdateQueue<any, any> | null,
next: Hook | null,
};
複製代碼
咱們都知道,在React v16版本推出了Fiber架構,每個節點都對應一個Fiber結構。一個Function Component中能夠有不少個Hooks執行,最終造成的結構以下:緩存
上面這個圖到這裏還看不懂不要緊,下面讓咱們開始深刻Hooks的原理。要知道這個問題的答案,首先須要瞭解React的Fiber架構。React定義了Fiber結構來描述一個節點,經過Fiber節點上的child、return、sibling指針構成整個App的結構。Fiber類型定義以下:bash
export type Fiber = {|
tag: WorkTag,
key: null | string,
elementType: any,
type: any,
stateNode: any,
return: Fiber | null,
child: Fiber | null,
sibling: Fiber | null,
index: number,
ref: null | (((handle: mixed) => void) & {_stringRef: ?string}) | RefObject,
pendingProps: any, // This type will be more specific once we overload the tag.
memoizedProps: any, // The props used to create the output.
updateQueue: UpdateQueue<any> | null,
memoizedState: any,
dependencies: Dependencies | null,
mode: TypeOfMode,
effectTag: SideEffectTag,
nextEffect: Fiber | null,
firstEffect: Fiber | null,
lastEffect: Fiber | null,
expirationTime: ExpirationTime,
childExpirationTime: ExpirationTime,
alternate: Fiber | null,
// ...
|};
複製代碼
用React Conf上的例子簡單說明一下 Fiber 結構:List組件下面有四個子節點:一個button和三個Item。閉包
這個組件轉換成Fiber tree結構以下:一個 Function Component 最終也會生成一個 Fiber 節點掛載到 Fiber tree 中。React 還使用了 Double Buffer 的思想,在 React 內部會維護兩棵 Fiber tree,一棵叫 Current,一棵叫 WorkInProgress,current 是最終展現在用戶界面上的結構,workInProgress 用於後臺進行diff更新操做。兩棵樹在更新結束的時候會互相調換,即 workInProgress 在更新以後會變爲 current 展現在用戶界面上,current 會變成 workInProgress 用於下次 update。兩棵樹之間對應的節點是經過Fiber結構上的 alternat e屬性連接的,即 workInProgress.alternate = current,current.alternate = workInProgress
。架構
那這和Hooks有什麼關係?其實React在初始渲染的時候,只會生成一棵workInProgress
樹,當整棵樹構建完成以後,由workInProgress變爲current,在下一次更新的時候纔會生成第二棵樹。因此當Function Component對應的Fiber節點發現本身的alternate屬性爲null,說明是第一次渲染。在React的源碼中就是renderWithHooks
函數中的這句(Function Component的mount和update過程會執行renderWithHooks
):ide
export function renderWithHooks( // 判斷是mount仍是update:fiber.memoizedState是否有值
current: Fiber | null,
workInProgress: Fiber,
Component: any,
props: any,
refOrContext: any,
nextRenderExpirationTime: ExpirationTime,
): any {
// ...
nextCurrentHook = current !== null ? current.memoizedState : null;
ReactCurrentDispatcher.current =
nextCurrentHook === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
// ...
}
複製代碼
從代碼中能看到current 等於 null的狀況下,nextCurrentHook = null
,致使下面的dispatcher取得是HooksDispatcherOnMount
,這個HooksDispatcherOnMount
就是在初始渲染Mount階段對應的Hooks,HooksDispatcherOnUpdate
顯然就是在更新階段時該調用的Hooks。
上面知道了Hooks是如何區分Mount和Update以後,接下來分析useState
的實現原理。
state在Function Component中就是一個普通的常量,不存在諸如數據綁定之類的邏輯,更新都是經過useState
的dispatch
(useState返回的數組的第二個元素),觸發了組件rerender,進而更新組件。
dispatch
方法接收到最新的state後(就是第三個參數action),生成一個update,添加到queue的最後,用last指針指向這最新的一次更新,而後調用scheduleWork
方法,這個scheduleWork
就是觸發react更新的起始方法,在Class Component中調用this.setState
時最終也是執行了這個方法開始更新。
function dispatchAction<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A, // 最新的state
) {
const alternate = fiber.alternate;
if (
fiber === currentlyRenderingFiber ||
(alternate !== null && alternate === currentlyRenderingFiber)
) {
// ...
} else {
const currentTime = requestCurrentTime();
const suspenseConfig = requestCurrentSuspenseConfig();
const expirationTime = computeExpirationForFiber(
currentTime,
fiber,
suspenseConfig,
);
const update: Update<S, A> = {
expirationTime,
suspenseConfig,
action,
eagerReducer: null,
eagerState: null,
next: null,
};
// Append the update to the end of the list.
const last = queue.last;
if (last === null) {
// This is the first update. Create a circular list.
update.next = update;
} else {
const first = last.next;
if (first !== null) {
// Still circular.
update.next = first;
}
last.next = update;
}
queue.last = update;
// ...
scheduleWork(fiber, expirationTime);
}
}
複製代碼
上面提到了Hooks的結構,每一次Hook的執行都會生成一個Hook結構,首次渲染的時候執行useState,會將傳過來的initialState
掛在Hook的memoizedState
屬性上,後續再獲取狀態就是從memoizedState
屬性上獲取了。
useState
最終會返回一個有兩個元素的數組,第一個元素是state,第二個元素是 修改state的方法 dispatch
。 這裏dispatch方法也須要關注一下:queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber, queue )
,dispatch是dispatchAction方法經過bind傳入當前的 Fiber 和 queue屬性做爲前兩個參數生成的,因此每一個useState都有本身的dispatch方法,這個dispatch方法是固定做用在指定的Fiber上的(經過閉包鎖定了前兩個參數)
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
const queue = (hook.queue = {
last: null,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
});
const dispatch: Dispatch<BasicStateAction<S>> = (queue.dispatch = (dispatchAction.bind(
null, currentlyRenderingFiber, queue ));
return [hook.memoizedState, dispatch];
}
複製代碼
queue屬性用一個Update結構記錄了每次調用dispatch時候傳過來的state,造成了一個鏈表結構,其last屬性指向最新的一個state Update結構。Update結構以下,咱們須要關注的有:
其餘屬性涉及到的內容不在本次討論範圍
update階段,React首先會從Hooks上獲取到 last、baseState、baseUpdate
屬性,各個值的含義以下:
last
:queue上掛載的最新一次的Update,裏面包含了最新的state,在dispatch方法執行的時候掛載到queue上的baseState
:上一次更新後的state值。當dispatch傳入的是一個函數的時候,這個值就是函數執行時傳入的參數baseUpdate
:上一次更新生成的Update結構,做用是找到本次rerender的第一個爲處理的Update節點(baseUpdate.next),即下面代碼中的first表明第一個未處理的updateconst hook = updateWorkInProgressHook();
const queue = hook.queue;
// The last update in the entire queue
const last = queue.last; // 新的值
// The last update that is part of the base state.
const baseUpdate = hook.baseUpdate; // 舊的值,next屬性指向新的Update
const baseState = hook.baseState; // 舊的值
// Find the first unprocessed update.
let first;
if (baseUpdate !== null) {
if (last !== null) {
last.next = null;
}
first = baseUpdate.next;
} else {
first = last !== null ? last.next : null;
}
複製代碼
找到第一個未處理的update以後就須要循環對全部新增update進行處理,這裏的變量newState雖然名字叫newState,在每次執行reducer以前的值都是 就的state值,因此當useState
傳入的值爲一個函數的時候,咱們能夠獲取到上一次的state,由於舊的state值是有緩存的。 當處理完全部update以後就更新hooks對應的baseState、baseUpdate、memoizedState
的值。
if (first !== null) {
let newState = baseState;
let newBaseState = null;
let newBaseUpdate = null;
let prevUpdate = baseUpdate;
let update = first;
let didSkip = false;
do {
// ...
const action = update.action;
newState = reducer(newState, action);
prevUpdate = update;
update = update.next;
} while (update !== null && update !== first);
if (!didSkip) {
newBaseUpdate = prevUpdate;
newBaseState = newState;
}
if (!is(newState, hook.memoizedState)) {
markWorkInProgressReceivedUpdate();
}
hook.memoizedState = newState;
hook.baseUpdate = newBaseUpdate;
hook.baseState = newBaseState;
queue.lastRenderedState = newState;
}
// reducer函數:
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S {
return typeof action === 'function' ? action(state) : action;
}
複製代碼
最終仍然返回的是一個數組結構,包含了最新的state和dispatch方法
const dispatch: Dispatch<A> = (queue.dispatch: any);
return [hook.memoizedState, dispatch];
複製代碼
這樣Function Component中獲取到的state就是最新的值,Function Component的更新實際上就是從新執行一次函數,獲得 jsx,剩下 reconcile 和 commit 階段的就與 class Component是同樣的了
咱們知道useEffect
傳入的函數是在繪製以後才執行的,因此當執行function component執行的時候確定不是 useEffect 的第一個函數參數執行的時候,那在執行useEffect的時候都作了什麼呢?
執行useEffect時是在爲後面作準備。useEffect會生成一個Effect結構,Effect結構以下:
type Effect = {
tag: HookEffectTag,
create: () => (() => void) | void,
destroy: (() => void) | void,
deps: Array<mixed> | null,
next: Effect,
};
複製代碼
create就是咱們傳入的參數,咱們若是想取消一個反作用的話是經過create執行返回的結果(仍然是一個函數),React會在內部調用這個函數。由於create函數是在 繪製以後執行的,因此這個時候Effect的destory是null,在後面真正執行create的時候會賦值destory。
生成了Effect結構以後就要將其掛載到Hooks的memorizedState
上,React不光將Effect掛載到了Hook結構上,也將其直接和Fiber掛鉤:
React會將全部的effect
造成一個環形鏈表,保存在FunctionComponentUpdateQueue
上,其lastEffect指向最新生成的effect
。 爲何要作一個環形鏈表保存全部的effect? 我認爲主要是:
最終處理上面保存的effects的函數在commit階段的commitHookEffectList
函數中,代碼以下,主要工做就是:循環處理全部的effect,判斷須要銷燬仍是須要執行,循環終止條件就是從新回到環形鏈表的第一個節點。刪減後的代碼以下:
// ReactFiberCommitWork.js
function commitHookEffectList(
unmountTag: number,
mountTag: number,
finishedWork: Fiber,
) {
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
let lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
if ((effect.tag & unmountTag) !== NoHookEffect) {
// Unmount
const destroy = effect.destroy;
effect.destroy = undefined;
if (destroy !== undefined) {
destroy();
}
}
if ((effect.tag & mountTag) !== NoHookEffect) {
// Mount
const create = effect.create;
effect.destroy = create();
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
複製代碼
useCallback
和useMemo
是十分類似(useCallback
能夠經過useMemo
實現),因此這裏咱們看一下useMemo
的實現: useMemo
傳入一個函數fn和一個數組dep,當dep中的值沒發生變化的時候,就會一直返回以前函數fn執行後返回的值,因此咱們首先執行函數fn,將返回的結果和依賴數組dep保存起來就行了, React 內部是這麼處理的:
const nextValue = nextCreate(); // 傳入的函數Fn
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
複製代碼
當觸發re-render的時候再次執行 useMemo
,React會從 hook.memoizedState
上面取出以前保存的dep,也就是hook.memoizedState
的第二個元素。比較以前的dep和新傳入的dep的每一個元素是否相同(淺比較),若是相同則返回原來保存的值(hook.memoizedState
的第一個元素),不相同則從新執行 函數Fn 從新生成返回值並保存。
const prevState = hook.memoizedState;
if (prevState !== null) {
if (nextDeps !== null) {
const prevDeps: Array<mixed> | null = prevState[1];
if (areHookInputsEqual(nextDeps, prevDeps)) {
return prevState[0];
}
}
}
const nextValue = nextCreate();
hook.memoizedState = [nextValue, nextDeps];
return nextValue;
複製代碼
useCallback
原理相同,只不過保存的值不一樣,一個是函數,一個是函數的執行結果。
useRef
的實現是最簡單的了,咱們先回顧一下useRef
的做用:使用useRef
生成一個對象以後能夠經過 current 屬性獲取到一個值,這個是不會隨着re-render而改變,只能咱們本身手動改變。固然也能夠傳遞給組件的 ref 屬性使用。常常被類比爲 Class Component的實例屬性,好比 this.xxx = xxx
。
在開頭部分咱們知道 Hooks 是基於 fiber 結構的,fiber 是 react 內部維護的結構,會在整個react生命週期中存在,因此useRef最簡單的實現就是 掛載到 fiber 上,做爲fiber的一個屬性存在,這樣經過 fiber 就一直能獲取到值。
可是真正設計的時候確定不能這樣來,由於 Hooks 是有本身的結構的,因此就把 useRef
的值掛載到 Hook 結構的 memorizedState
上 就能夠了,因此你看到的 useRef
的結構是這樣的:
current
屬性上,從
current
屬性上能獲取到值
React.memo is equivalent to PureComponent, but it only compares props. (You can also add a second argument to specify a custom comparison function that takes the old and new props. If it returns true, the update is skipped.)
使用 React.memo包裹組件以後,當父組件傳過來的props不變時,子組件不會re-render。舉個例子:
const ChildComponent = React.memo((props) => {
return (
<div>{props.name}</div>
)
}, compare)
複製代碼
React.memo是對新舊Props進行淺比較,也能夠自定義compare函數比較nextProps
和prevProps
,淺比較就會帶來問題:每次Function Component執行內部的對象都會從新生成,這個時候若是傳給子組件的是一個對象的話,其實仍是會形成刷新。例:
function ParentComponent() {
const [state, setState] = useState(0)
function handleClickButton() {
console.log(state)
// balabalabala
}
const someProps = {
name: 'xxx'
}
return (
<div>
<input />
<ChildExpensiveComponent onPress={handleClickButton} someProps={someProps}/>
</div>
)
}
複製代碼
因此這裏咱們想到用 useMemo
和 useCallback
來保存值,這樣只要依賴不變值就不變。因此下面代碼這種改變以後確實不會觸發沒必要要的刷新了
function ParentComponent() {
const [state, setState] = useState(0)
const handleClickButton = useCallback(() => {
// balabalabala
console.log(state) // 0
}, [])
const memoProps = useMemo(() => {
return {
name: 'xxx'
}
}, [])
return (
<div>
<p>{state}</p>
<ChildExpensiveComponent onPress={handleClickButton} someProps={memoProps}/>
</div>
)
}
複製代碼
可是Hooks是經過closure實現的,除useRef以外,其餘的Hooks都會存在capture values的特色。上面例子中handleClickButton
每次執行,不管state
如何變化,打印的state
值將一直是0。 由於useCallback
經過閉包保存了一開始的state的值,這個值不會像Class Component同樣每次都會取到最新的值。
那咱們是否是給handleClick
加個dependence就好了,像這樣:
const handleClickButton = useCallback(() => {
// balabalabala
console.log(state) // 0
}, [state])
複製代碼
可是當你的state頻繁發生變化的時候,handleClickButton
其實會頻繁改變,這樣的話你的子組件經過React.memo
實現的優化就失效了。
因此當依賴常常變更時,盲目使用useCallback
或useMemo
可能會致使性能不升反降。 上面咱們已經瞭解了,在react內部,useCallback
執行會生成一個Hook結構,將函數和deps保存在這個Hook結構的memoizedState
上,在每次rerender的時候,react會去比較prevDeps
和nextDeps
,相等會返回保存的值/函數,不相等會從新執行,因此當deps頻繁改變的時候,會多了一個比較deps是否改變的操做,也會浪費性能。這裏有一個來自React官網的例子:
function Form() {
const [text, updateText] = useState('');
const handleSubmit = useCallback(() => {
console.log(text);
}, [text]); // 每次 text 變化時 handleSubmit 都會變
return (
<>
<input value={text} onChange={(e) => updateText(e.target.value)} />
<ExpensiveTree onSubmit={handleSubmit} /> // 很重的組件
</>
);
}
複製代碼
解決這種問題React官方也給了一種方式,就是使用useRef
:
function Form() {
const [text, updateText] = useState('');
const textRef = useRef();
useEffect(() => {
textRef.current = text; // Write it to the ref });
const handleSubmit = useCallback(() => {
const currentText = textRef.current; // Read it from the ref
alert(currentText);
}, [textRef]); // Don't recreate handleSubmit like [text] would do return ( <> <input value={text} onChange={e => updateText(e.target.value)} /> <ExpensiveTree onSubmit={handleSubmit} /> </> ); } 複製代碼
當使用Hooks時不要把思惟仍然侷限在class Component的定式中,useRef一般給人的感受是和class Component的createRef相似的做用,但 useRef 除了在 ref 使用以外,還能夠用來保存值,上面這個例子是一個很是好的例子,幫咱們更好的去使用Hooks避免一些性能的浪費。
zhuanlan.zhihu.com/p/142735113
歡迎關注個人我的技術公衆號,不按期分享各類前端技術~