我只是不能忍受。。。。你一生在這裏打轉。你太聰明,太有趣。。。你只有一次生命,應該儘可能活的充實。react
基於reactV17.0源碼分析數組
本片文章主要經過react的源碼,分析useState、useEffect和兩個鉤子函數的內在執行原理。緩存
爲了解決包括不限於以上的問題,咱們打開react項目研究一下源碼。限於我的技術緣由,若有問題戳我。markdown
領略react-hook以前,咱們先回顧一下從reactDom.render(element, dom)
開始到hook
節點建立中間經歷了那些步驟。下面是我根據源碼,將初次渲染的關鍵路徑簡單的梳理了一下,只涉及關鍵函數。(打開react項目,點點點)閉包
renderWithHooks
顧名思義,這個函數就是對hook
組件的處理函數的入口。下面簡單的分析一下這個函數主要作了什麼:dom
currentlyRenderFiber
指向workInProgress
memoizedState
判斷首次掛載/更新,獲取hooks
方法集的Dispatcher
對象,並將此對象賦值給全局變量ReactCurrentDispatcher
的current
屬性hook
組件的function
構造方法// packages/react-reconciler/src/ReactFiberHooks.old.js
export function renderWithHooks() {
currentlyRenderingFiber = workInProgress;
workInProgress.memoizedState = null;
workInProgress.updateQueue = null;
workInProgress.lanes = NoLanes;
...
//賦值全局變量,能夠在react中使用相關hook方法
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
let children = Component(props, secondArg);
// 省略一些條件的判斷,和置空
return children;
}
複製代碼
react
模塊中定義,並暴露,因此咱們能import { useState, useEffect } from 'react'
renderWithHooks
函數實現各類鉤子函數,並將各個鉤子函數掛在對應的fiber
對象上的hook
鏈表hook
的構造方法(Component()
)時,可以使用相關的hook
方法實現並掛載在全局變量ReactCurrentDispatcher
以後,在執行hook組件的構造方法時,咱們就能拿到相關的hook方法。咱們就先拿最多見的useState
和useEffect
來講明,其餘的鉤子函數先不考慮。函數
// packages/react/src/ReactCurrentDispatcher.js
//在react模塊中定義,並拋出了這個對象
const ReactCurrentDispatcher = {
current: (null: null | Dispatcher),
};
export default ReactCurrentDispatcher;
//packages/react-reconciler/src/ReactFiberHooks.old.js
//在react-reconciler模塊中實現
const HooksDispatcherOnMount: Dispatcher = {
//初次掛載
useState: mountState,
useEffect: mountEffect,
};
const HooksDispatcherOnUpdate: Dispatcher = {
//更新
useEffect: updateEffect,
useState: updateState,
};
複製代碼
useState
源碼裏咱們能夠看到,最關鍵的就是利用bind
閉包,緩存了currentlyRenderingFiber
對象(我也列出了源碼中對此對象的解釋,就是當前的fiber
節點)和queue
對象(保存的是更新對象update
,在dispatchAction
中咱們能夠看到)源碼分析
// The work-in-progress fiber. I've named it differently to distinguish it from
// the work-in-progress hook.
let currentlyRenderingFiber: Fiber = (null: any);
...
function mountState(initialState,){
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
// $FlowFixMe: Flow doesn't like mixed types
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
const queue = (hook.queue = {
pending: null,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: (initialState: any),
});
// 利用閉包緩存
const dispatch: Dispatch<
BasicStateAction<S>,
> = (queue.dispatch = (dispatchAction.bind(
null,
currentlyRenderingFiber,
queue,
): any));
return [hook.memoizedState, dispatch];
}
複製代碼
接下來咱們能夠來看看hook對象的建立mountWorkInProgress
函,在這個函數裏主要就是構造了一個hook的單項鍊表,並將workInProgressHook
指針指向當前hook
。對於workInProgressHook
對象,源碼中也有詳細的解釋,用以保存將要加進當前fiber
對象的hook
鏈表優化
// Hooks are stored as a linked list on the fiber's memoizedState field. The
// current hook list is the list that belongs to the current fiber. The
// work-in-progress hook list is a new list that will be added to the
// work-in-progress fiber.
let currentHook: Hook | null = null;
let workInProgressHook: Hook | null = null;
function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};
if (workInProgressHook === null) {
// This is the first hook in the list
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
// Append to the end of the list
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
複製代碼
咱們先來看一段實例代碼ui
function MyName(){
const [name, setName] = useState()
return <div> <span>{name}</span> <button onClick={setName('駱家豪')}> 展現名字</button> </div>
}
複製代碼
看了上面初次掛載的解析後,咱們明白setName
就是dispatchAction
函數,而且利用閉包緩存了當前fiber對象的指引。so,咱們來看看最最核心的dispatchAction
函數作了什麼騷操做。
function dispatchAction<S, A>( fiber: Fiber, queue: UpdateQueue<S, A>, action: A, ) {
//調用優先級相關,可看以前的文章
const eventTime = requestEventTime();
const lane = requestUpdateLane(fiber);
//建立一個更新任務
const update: Update<S, A> = {
lane,
action,
eagerReducer: null,
eagerState: null,
next: (null: any),
};
// 將更新加到鏈表的最後
const pending = queue.pending;
if (pending === null) {
// 這是第一個更新,建立一個環形鏈表
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
queue.pending = update;
...
//發起調度
scheduleUpdateOnFiber(fiber, lane, eventTime);
}
複製代碼
咱們能夠看到其實就是構造了一個update
的鏈式結構,並掛在hook
對象上的queue
屬性上,並在最後發起一個調度。那麼調度以後是如何的從新計算fiber
節點的,如何處理queue
中的update
更新鏈,咱們還要看updateState
函數。
function updateState<S>( initialState: (() => S) | S, ): [S, Dispatch<BasicStateAction<S>>] {
return updateReducer(basicStateReducer, (initialState: any));
}
複製代碼
直接返回一個updateReducer
函數,顧名思義,就是一個更新合併的函數。將一次或屢次setName
函數產生的update
合併。計算出最新的state
。
function updateReducer(){
//1.合併並計算queue隊列
let baseQueue = current.baseQueue;
//2.若是存在等待的更新隊列,則循環update的單向鏈表,reducer全部的state
if(baseQueue !== null){
const first = baseQueue.next;
let newState = current.baseState;
let update = first;
do {
const action = update.action;
newState = reducer(newState, action);
update = update.next;
} while (update !== null && update !== first)
}
//3.將最新的state存入當前hook,並返回
hook.memoizedState = newState;
hook.baseState = newBaseState;
hook.baseQueue = newBaseQueueLast;
const dispatch: Dispatch<A> = (queue.dispatch: any);
return [hook.memoizedState, dispatch];
}
複製代碼
到此咱們將useState
這個hook
的建立,更新過程過了一遍。下面來技術總計。
hook
對象以鏈表的形式保存在當前fiber
對象的memoizedState
屬性上,造成環形結構。便於以後的便利合併。useState
鉤子函數返回的第二個參數setXxx
,其實就是利用閉包,對應源碼dispatchAction.bind(null, currentlyRenderingFiber,queue,)
。緩存了當前的fiber
對象和queue
跟新隊列,這也解釋了爲何hook
函數不須要this
指針也能對應上指定的更新對象setXxx
時,也就是建立了一個update
,並將update
對象以單項鍊表的形式保存在當前hook
的queue
屬性上。並在最後發起一個調度scheduleUpdateOnFiber
,全部更新的入口函數。memoizedState
,就會執行updateState
,也就是執行了updateReducer
函數,顧名思義就是合併更新updateReducer
,在這個函數中將update
合併,並do-while
遍歷update
,合併計算update
對象中的action
屬性,就是setXxx
的第一個參數,能夠是函數。最後返回一個新的state
保存在當前hook的memoizedState
屬性中。看完useState
的代碼,在看useEffect
函數,大部分都是相同的。最大的不一樣仍是觸發時機的不一樣,useEffect
在render
過程以後觸發計算,useState
在下次計算當前fiber的時候觸發執行計算newState
。
咱們先一塊兒來看看 useEffect
初次掛載,和更新時的代碼。
function mountEffect(create,deps) {
return mountEffectImpl(
UpdateEffect | PassiveEffect,
HookPassive,
create,
deps,
);
}
function updateEffect(create,deps) {
return updateEffectImpl(
UpdateEffect | PassiveEffect,
HookPassive,
create,
deps,
);
}
複製代碼
看了代碼其實區別就轉換成了mountEffectImpl
和updateEffectImpl
區別。
function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
currentlyRenderingFiber.flags |= fiberFlags;
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
undefined,
nextDeps,
);
}
function updateEffectImpl(fiberFlags, hookFlags, create, deps): void {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
let destroy = undefined;
if (currentHook !== null) {
const prevEffect = currentHook.memoizedState;
destroy = prevEffect.destroy;
if (nextDeps !== null) {
const prevDeps = prevEffect.deps;
if (areHookInputsEqual(nextDeps, prevDeps)) {
pushEffect(hookFlags, create, destroy, nextDeps);
return;
}
}
}
currentlyRenderingFiber.flags |= fiberFlags;
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
destroy,
nextDeps,
);
}
複製代碼
看了上面的掛載、更新兩個階段的函數對比,咱們很容易發現,更新階段僅僅比掛載階段多了一段判斷依賴數組是否相同的代碼,多作了一個減小重複執行的優化(hook
第一個參數create
)。全部在咱們平時開發中必定要注意useEffect
第二個參數的使用,固然源碼中的比較函數,也只是作了一層淺比較。
那麼接下來咱們就打開pushEffect
函數看看狀況。
function pushEffect(tag, create, destroy, deps) {
const effect: Effect = {
tag,
create,
destroy,
deps,
// 環形鏈表
next: (null: any),
};
let componentUpdateQueue: null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue: any);
if (componentUpdateQueue === null) {
componentUpdateQueue = createFunctionComponentUpdateQueue();
currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
const lastEffect = componentUpdateQueue.lastEffect;
if (lastEffect === null) {
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
const firstEffect = lastEffect.next;
lastEffect.next = effect;
effect.next = firstEffect;
componentUpdateQueue.lastEffect = effect;
}
}
return effect;
}
複製代碼
其實看完很簡單,並無複雜的合併,計算。只是將生成一個effect
對象,並以環形鏈表的結構存在hook
的memoizedState
屬性上,並將此鏈表賦值給全局變量componentUpdateQueue
。
最中造成一個effect
的環形鏈表後,放在了componentUpdateQueue
中。至於什麼時候觸發,找了一會,在commitHookEffectListMount
函數中找到了對hook-effect
的處理
下面咱們就詳細的看下
commitHookEffectListMount
如何處理effect
鏈表
function commitHookEffectListMount(tag: number, finishedWork: Fiber) {
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
// 作了判斷,過濾了依賴重複時,push進來的effect
if ((effect.tag & tag) === tag) {
// Mount
const create = effect.create;
effect.destroy = create();
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
複製代碼
其實很簡單,就是遍歷effect鏈表,並利用tag過濾依賴dept
沒變產生的effect
。執行create
函數,而後將create
函數執行的結果賦值給destory
屬性。
而後繼續在突變階段-commitWork
函數找到了destory
執行
function commitHookEffectListUnmount(tag: number, finishedWork: Fiber) {
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
if ((effect.tag & tag) === tag) {
// Unmount
const destroy = effect.destroy;
effect.destroy = undefined;
if (destroy !== undefined) {
destroy();
}
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
複製代碼
useState
,仍是useEffect
都是產生一個鏈表掛載在當前hook
對象的memoizedState
屬性上。useState
產生的是update
更新對象的鏈表,useEffect
產生的是effect
反作用鏈表useState
在下次更新時,合併計算hook
對象上的update
鏈表,最中計算出最新的newState
,賦值給hook.momoizedState
;而useEffect
則在commit
階段的突變後纔開始執行hook
上的effect
鏈表的create函數,在commit
階段的突變時執行destory函數