本文首發於公衆號:符合預期的CoyPanjavascript
在上一篇文章中,java
主要分析了Hook在React中是如何保存的,以及Hook的更新過程。本文中,咱們將經過下面兩個問題,繼續深刻研究Hook,以彌補上文中略過的一些細節。閉包
一、若是我連續屢次調用setState
,Hook會怎麼處理呢?ide
二、Hook的useEffect 是如何工做的?函數
先看示例代碼:post
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。ui
在上一篇源碼解析中,這部份內容被忽略了。本文咱們來看看這裏的內部邏輯。spa
首先,先複習一下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
後,會構造一個環形結構:
第二次、第三次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);
...
複製代碼
首先上示例代碼:
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;
}
複製代碼
回到本文開頭的兩個問題:
一、若是我連續屢次調用setState
,Hook會怎麼處理呢?
二、Hook的useEffect 是如何工做的?
對於每個hook,react會在hook對象的queue字段上,以有環鏈的形式,存儲更新信息。連續屢次更新,會沿着queue鏈計算出最新該hook最新的值。
使用useEffect,也會生成一個hook對象。只是該hook對象與useState生成的hook對象有區別。組件從新渲染時,會判斷傳入useEffect的dep依賴是否與上一次相同,相同的話,則會爲這次更新打上特殊的tag,保證不會執行useEffect中傳入的函數。
本文在前一篇文章的基礎上,進一步分析了hook中state的更新機制。另外,大體分析了useEffect是如何存儲,如何工做的。因爲本文不涉及react的調度更新過程,看起來不太連貫,請多包涵。關於react hook的更多解析,請關注我後續的文章。