本文首發於公衆號:符合預期的CoyPan
在上一篇文章中,主要分析了Hook在React中是如何保存的,以及Hook的更新過程。本文中,咱們將經過下面兩個問題,繼續深刻研究Hook,以彌補上文中略過的一些細節。javascript
一、若是我連續屢次調用setState
,Hook會怎麼處理呢?java
二、Hook的useEffect 是如何工做的?react
先看示例代碼:閉包
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。ide
在上一篇源碼解析中,這部份內容被忽略了。本文咱們來看看這裏的內部邏輯。函數
首先,先複習一下hook的結構:ui
var hook = { memoizedState: null, // 當前的state值 baseState: null, queue: null, // 存儲更新信息 baseUpdate: null, next: null // 指向下一個hook對象的指針 };
咱們先看一下組件掛載完成後的hook,注意queue
字段的值:spa
以前講過,調用setCount
的時候,實際上調用的是dispatchAction.bind(null, currentlyRenderingFiber$1, queue)
這個函數。這個函數經過閉包保存了對應的Fiber和hook對象的queue的引用。3d
首次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的更多解析,請關注我後續的文章。