React Hook原始碼解析(二)
ID: 符合預期的CoyPan
CoyPan,BAT某廠符合預期的FE,正努力成為一名出色的工程師
寫在前面
在上一篇文章中,主要分析了Hook在React中是如何儲存的,以及Hook的更新過程。本文中,我們將透過下面兩個問題,繼續深入研究Hook,以彌補上文中略過的一些細節。
1、如果我連續多次呼叫setState
,Hook會怎麼處理呢?
2、Hook的useEffect 是如何工作的?
連續多次setState
先看示例程式碼:
const App = () => {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
setCount(count + 2);
setCount(count + 3);
};
return <React.Fragment>
<span style={{ marginRight: '10px' }}>{count}</span>
<button onClick={handleClick}>點選</button>
</React.Fragment>
};
我們點選一次button,最終頁面上會輸出多少呢?熟悉React的朋友們,很快就會得到答案:3。
在上一篇原始碼解析中,這部分內容被忽略了。本文我們來看看這裡的內部邏輯。
首先,先複習一下hook的結構:
var hook = {
memoizedState: null, // 當前的state值
baseState: null,
queue: null, // 儲存更新資訊
baseUpdate: null,
next: null // 指向下一個hook物件的指標
};
我們先看一下元件掛載完成後的hook,注意queue
欄位的值:
之前講過,呼叫setCount
的時候,實際上呼叫的是dispatchAction.bind(null, currentlyRenderingFiber$1, queue)
這個函式。這個函式透過閉包儲存了對應的Fiber和hook物件的queue的引用。
首次setCount
的時候,hook對queue的處理如下:
// 更新資訊
var _update2 = {
expirationTime: expirationTime, // Fiber排程相關
suspenseConfig: suspenseConfig,
action: action, // setCount函式接受的引數
eagerReducer: null,
eagerState: null,
next: null
};
var last = queue.last;
// 首次更新時
if (last === null) {
// This is the first update. Create a circular list.
// 第一次更新,構建一個 環
_update2.next = _update2;
} else {
// 後續更新
var first = last.next;
if (first !== null) {
// Still circular.
_update2.next = first;
}
last.next = _update2;
}
// queue的最近一次更新指向_update2
queue.last = _update2;
第一次更新後,會構造一個環形結構:
第二次、第三次setCount
時,會繼續構造queue鏈:
var first = last.next;
if (first !== null) {
// Still circular.
_update2.next = first;
}
last.next = _update2;
最終會形成下圖的結構:
元件重新渲染時,react會從hook的queue鏈中,找到最新的值,賦值給hook的memoizedState,我們就可以拿到最新的state了:
// 程式碼有省略
...
// 迴圈,直到拿到queue鏈上最新的值
do {
var updateExpirationTime = _update.expirationTime;
if (updateExpirationTime < renderExpirationTime$1) {
...
} else {
...
if (_update.eagerReducer === reducer) {
...
} else {
var _action = _update.action;
_newState = reducer(_newState, _action);
}
}
prevUpdate = _update;
_update = _update.next;
} while (_update !== null && _update !== first);
hook.memoizedState = _newState; // 最新的state值,本例中為3
hook.baseUpdate = newBaseUpdate; // 最新的基礎更新資訊,action=3
hook.baseState = newBaseState; // 最新的基礎state值,本例中為3
queue.lastRenderedState = _newState; // 最近渲染的state值,本例中為3
return [hook.memoizedState, dispatch];
這裡有一個注意事項,在上一篇文章中,我們提到過,setState中是支援傳入函式的。假設我們在setState中傳入的引數是一個函式,在本例中,如果我們點選按鈕後的程式碼改成:
const handleClick = () => {
setCount(count => count + 1);
setCount(count => count + 2);
setCount(count => count + 3);
};
最終的count值就不是3了,而是6。這是因為傳入reducer的是最新的state:
...
do {
var action = update.action; // 這裡的action是我們傳入的回撥函式
newState = reducer(newState, action); // newState 是最新的 state
update = update.next; // 取hook物件queue鏈上的下一次更新
} while (update !== null);
...
useEffect是如何工作的
首先上示例程式碼:
const fakeReq = function(input) {
return new Promise( resolve => {
setTimeout(() => {
resolve(`${input} - ${Date.now()}`);
}, 500);
});
}
const App = () => {
const [input, setInput] = useState('');
const [res, setRes] = useState('');
useEffect(() => {
fakeReq(input).then(res => {
setRes(res);
});
},[input]);
return <React.Fragment>
<input value={input} onChange={e => setInput(e.target.value)} />
<div>
返回結果為:<span>{res}</span>
</div>
</React.Fragment>
};
上面的程式碼中,我們在輸入框進入輸入的同時,會發起一個請求,並且將返回的結果顯示在頁面上。首先,我們來看看React是怎麼儲存useEffect
的。
在程式碼中,呼叫useEffect
後,同樣會生成一個hook物件,只是這個hook物件的memoizedState欄位不太一樣:
...
// fiberEffectTag 和 hookEffectTag 是兩個標識
// create、deps是我們傳入useEffect的兩個引數
function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps) {
var hook = mountWorkInProgressHook();
var nextDeps = deps === undefined ? null : deps;
sideEffectTag |= fiberEffectTag;
// useEffect生成的hook物件的memoizedState是一個特殊的物件
hook.memoizedState = pushEffect(hookEffectTag, create, undefined, nextDeps);
}
...
我們來看看pushEffect
幹了什麼:
function pushEffect(tag, create, destroy, deps) {
// effect物件
var effect = {
tag: tag,
create: create,
destroy: destroy,
deps: deps,
// Circular
next: null
};
// componentUpdateQueue是一個全域性變數,用來儲存元件的最新的副作用
if (componentUpdateQueue === null) {
componentUpdateQueue = createFunctionComponentUpdateQueue();
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
var lastEffect = componentUpdateQueue.lastEffect;
if (lastEffect === null) {
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
var firstEffect = lastEffect.next;
lastEffect.next = effect;
effect.next = firstEffect;
componentUpdateQueue.lastEffect = effect;
}
}
return effect;
}
構造一個帶環的鏈:
在本例中,初始化完成後,最終Fiber物件的hook鏈為:
當我們在輸入框進行輸入時,來看看useEffect是如何起作用的。
輸入時,會觸發元件的重新渲染,假設我們輸入了3,此時傳入useEffect的依賴變成了:
useEffect(() => {
fakeReq(input).then(res => {
setRes(res);
});
},['3']);
useEffect
的更新程式碼為:
function updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps) {
// 當前處理的hook
var hook = updateWorkInProgressHook();
// 最新傳入的依賴
var nextDeps = deps === undefined ? null : deps;
var destroy = undefined;
if (currentHook !== null) {
// 上一次的effect
var prevEffect = currentHook.memoizedState;
destroy = prevEffect.destroy;
if (nextDeps !== null) {
var prevDeps = prevEffect.deps;
// 對比兩次依賴是否相同。如果相同,則在componentUpdateQueue上增加一個 tag = NoEffect$1 的 effect。這裡的 NoEffect$1 是一個常量, 值為 0。 這裡很重要
if (areHookInputsEqual(nextDeps, prevDeps)) {
pushEffect(NoEffect$1, create, destroy, nextDeps);
return;
}
}
}
sideEffectTag |= fiberEffectTag;
// 如果兩次依賴不同,在 componentUpdateQueue 上增加一個 effect,並且更新hook的memorizedState
hook.memoizedState = pushEffect(hookEffectTag, create, destroy, nextDeps);
}
經過React的排程,會在 commitHookEffectList 這個函式中,判斷是否需要執行 useEffect 中傳入的函式:
function commitHookEffectList(unmountTag, mountTag, finishedWork) {
var updateQueue = finishedWork.updateQueue;
var lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
var firstEffect = lastEffect.next;
var effect = firstEffect;
do {
if ((effect.tag & unmountTag) !== NoEffect$1) {
// Unmount
var destroy = effect.destroy;
effect.destroy = undefined;
if (destroy !== undefined) {
destroy();
}
}
if ((effect.tag & mountTag) !== NoEffect$1) {
// Mount
var create = effect.create;
effect.destroy = create();
{
var _destroy = effect.destroy;
if (_destroy !== undefined && typeof _destroy !== 'function') {
var addendum = void 0;
if (_destroy === null) {
addendum = ' You returned null. If your effect does not require clean ' + 'up, return undefined (or nothing).';
} else if (typeof _destroy.then === 'function') {
...
} else {
addendum = ' You returned: ' + _destroy;
}
...
}
}
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
NoEffect$1
是一個等於0的全域性常量,從上面程式碼的do...while...
部分可以看到,當一個 effect 的 tag 為 0時,和任何變數做與運算,值都為0,不會進行任何操作。而上面的分析也提到了,useEffect的dep沒有變時,會宣告一個 tag = NoEffect$1
的effect。因此,useEffect的dep沒有變化時,useEffect的函式不會被執行。
我們再來看看,react是怎麼比較兩次的deps是否相同的:
// useEffect中傳入的Deps是否相同
function areHookInputsEqual(nextDeps, prevDeps) {
...
for (var i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
// 這裡的 is$1 ,就是 Object.is 這個方法
if (is$1(nextDeps[i], prevDeps[i])) {
continue;
}
return false;
}
return true;
}
總結
回到本文開頭的兩個問題:
1、如果我連續多次呼叫setState
,Hook會怎麼處理呢?
2、Hook的useEffect 是如何工作的?
對於每一個hook,react會在hook物件的queue欄位上,以有環鏈的形式,儲存更新資訊。連續多次更新,會沿著queue鏈計算出最新該hook最新的值。
使用useEffect,也會生成一個hook物件。只是該hook物件與useState生成的hook物件有區別。元件重新渲染時,會判斷傳入useEffect的dep依賴是否與上一次相同,相同的話,則會為此次更新打上特殊的tag,保證不會執行useEffect中傳入的函式。
寫在後面
本文在前一篇文章的基礎上,進一步分析了hook中state的更新機制。另外,大致分析了useEffect是如何儲存,如何工作的。由於本文不涉及react的排程更新過程,看起來不太連貫,請多包涵。關於react hook的更多解析,請關注我後續的文章。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/4662/viewspace-2825142/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- React原始碼解析(二):react-elementReact原始碼
- React 原始碼解析系列 - React 的 render 階段(二):beginWorkReact原始碼
- React原始碼解析React原始碼
- react——js原始碼解析ReactJS原始碼
- Webpack原始碼基礎-Tapable從使用Hook到原始碼解析Web原始碼Hook
- React Fiber 原始碼解析React原始碼
- React Hooks 原始碼解析(譯)ReactHook原始碼
- React-Router 原始碼解析React原始碼
- React Hooks原始碼深度解析ReactHook原始碼
- react-redux原始碼解析ReactRedux原始碼
- React原始碼解析(三):react-componentReact原始碼
- React原始碼解析(二):元件的型別與生命週期React原始碼元件型別
- React Hooks 原始碼解析(3):useStateReactHook原始碼
- redux && react-redux原始碼解析ReduxReact原始碼
- react-loadable 原始碼解析React原始碼
- React-原始碼解析-DOM模型React原始碼模型
- 《React原始碼解析》系列完結!React原始碼
- react hookReactHook
- 【原始碼解析】React Native元件渲染原始碼React Native元件
- React-Redux 原始碼解析 一(createStore)ReactRedux原始碼
- 深度解析 create-react-app 原始碼ReactAPP原始碼
- React原始碼解析(四):事件系統React原始碼事件
- Picasso-原始碼解析(二)原始碼
- Mobx 原始碼解析 二(autorun)原始碼
- FaceBook POP原始碼解析二原始碼
- Flutter aspectd (二)原始碼解析Flutter原始碼
- ThreadPoolExecutor原始碼解析(二)thread原始碼
- create-react-app 原始碼解析之react-scriptsReactAPP原始碼
- 【原始碼SOLO】Retrofit2原始碼解析(二)原始碼
- Hook踩坑記:React Hook react-unity-webglHookReactUnityWeb
- react解析 React.Children(二)React
- 窺探React-原始碼分析(二)React原始碼
- React原始碼解析(2):元件的掛載React原始碼元件
- React原始碼解析(一):JSX到javascriptReact原始碼JSJavaScript
- Glide原始碼解析二---into方法IDE原始碼
- RxJava2 原始碼解析(二)RxJava原始碼
- React Native 0.55.4 Android 原始碼分析(Java層原始碼解析)React NativeAndroid原始碼Java
- React 原始碼解析系列 - React 的 render 階段(三):completeUnitOfWorkReact原始碼