React Hooks 源碼解析(4):useEffect
- 2019 年 12 月 24 日
- 筆記
- React 源碼版本: v16.11.0
- 源碼注釋筆記:airingursb/react
1. useEffect 簡介
1.1 為什麼要有 useEffect
我們在前文中說到 React Hooks 使得 Functional Component 擁有 Class Component 的特性,其主要動機包括:
- 在組件之間復用狀態邏輯很難
- 複雜組件變得難以理解
- 難以理解的 class
對於第二點,首先,針對 Class Component 來說,我們寫 React 應用時經常要在組件的各種生命周期中編寫程式碼,如在 componentDidMount
和 componentDidUpdate
中發送 HTTP 請求、事件綁定、甚至做一些額外的邏輯,使得業務邏輯扎堆在組件的生命周期函數中。在這個時候,我們的編程思路是「在組件裝載完畢時我們需要做什麼」、「在組件更新時我們需要做什麼」,這使得 React 開發成為了面向生命周期編程,而我們在生命周期中寫的那些邏輯,則成了組件生命周期函數的副作用。
其次,面向生命周期編程會導致業務邏輯散亂在各生命周期函數里。比如,我們在 componentDidMount
進行的事件綁定又需要在 componentDidUnmount
解綁,那事件管理的邏輯就不統一,程式碼零散 review 起來會比較麻煩:
import React from 'react'class A extends React.Componment { componmentDidMount() { document.getElementById('js_button') .addEventListener('click', this.log) } componentDidUnmount() { document.getElementById('js_button') .removeEventListener('click', this.log) } log = () => { console.log('log') } render() { return ( <div id="js_button">button</div> ) }}
而 useEffect
的出現,則讓開發者的關注點從生命周期重新抽離出來聚焦在業務邏輯之上,其實 effect 的全稱就是 side effect,即副作用,useEffect 就是用來處理原本生命周期函數里的副作用邏輯。
接下來,我們看看 useEffect 的用法。
1.2 useEffect 的用法
上面那段程式碼用 useEffect 改寫之後如下:
import React, { useEffect } from 'react'function A() { log() { console.log('log') } useEffect(() => { document .getElementById('js_button') .addEventListener('click', log) return () => { document .getElementById('js_button') .removeEventListener('click', log) } }) return (<div id="js_button">button</div>)}
useEffect 接受兩個參數,第一個參數是一個 function,其實現 bind 操作並將 unbind 作為一個 thunk 函數被返回。第二個參數是一個可選的 dependencies 數組,如果dependencies 不存在,那麼 function 每次 render 都會執行;如果 dependencies 存在,只有當它發生了變化,function 才會執行。由此我們也可以推知,如果 dependencies 是一個空數組,那麼當且僅當首次 render 的時候才會執行 function。
useEffect( () => { const subscription = props.source.subscribe(); return () => { subscription.unsubscribe(); }; }, [props.source],);
更多用法請閱讀 React 官網的 useEffect API 介紹: https://reactjs.org/docs/hooks-reference.html#useeffect
2. useEffect 的原理與簡單實現
根據 useEffect 的用法,我們可以自己實現一個簡單的 useEffect:
let _deps; function useEffect(callback, dependencies) { const hasChanged = _deps && !dependencies.every((el, i) => el === _deps[i]) || true; // 如果 dependencies 不存在,或者 dependencies 有變化,就執行 callback if (!dependencies || hasChanged) { callback(); _deps = dependencies; }}
3. useEffect 源碼解析
3.1 mountEffect & updateEffect
useEffect 的入口和上一節中 useState 的一樣,都在 ReactFiberHooks.js 這個文件中,並且同 useState 一樣,在首次載入時 useEffect 實際執行的是 mountEffect,之後每次渲染執行的是 updateEffect,此處不再贅述。那我們需要重點看看 mountEffect 和 updateEffect 實際做了什麼。
對於 mountEffect:
function mountEffect( create: () => (() => void) | void, deps: Array<mixed> | void | null,): void { return mountEffectImpl( UpdateEffect | PassiveEffect, UnmountPassive | MountPassive, create, deps, );}
對於 updateEffect:
function updateEffect( create: () => (() => void) | void, deps: Array<mixed> | void | null,): void { return updateEffectImpl( UpdateEffect | PassiveEffect, UnmountPassive | MountPassive, create, deps, );}
mountEffect 和 updateEffect 的入參是一個 function 和一個 array,對應的就是我們前文 useEffect 傳的 callback 和 deps。同時,我們可以發現 mountEffect 和 updateEffect 實際調用的是 mountEffectImpl 和 updateEffectImpl,它們接受的四個參數一模一樣的,後面兩個參數直接透傳的不用說,主要是前面的 UpdateEffect|PassiveEffect
、 UnmountPassive|MountPassive
究竟是什麼?
閱讀程式碼可知他們是從 ReactSideEffectTags
與 ReactHookEffectTags
中引入的。
import { Update as UpdateEffect, Passive as PassiveEffect,} from 'shared/ReactSideEffectTags';import { NoEffect as NoHookEffect, UnmountPassive, MountPassive,} from './ReactHookEffectTags';
看一下 ReactSideEffectTags.js 與 ReactHookEffectTags.js 中的定義:
// Don't change these two values. They're used by React Dev Tools.export const NoEffect = /* */ 0b0000000000000;export const PerformedWork = /* */ 0b0000000000001; // You can change the rest (and add more).export const Placement = /* */ 0b0000000000010;export const Update = /* */ 0b0000000000100;export const PlacementAndUpdate = /* */ 0b0000000000110;export const Deletion = /* */ 0b0000000001000;export const ContentReset = /* */ 0b0000000010000;export const Callback = /* */ 0b0000000100000;export const DidCapture = /* */ 0b0000001000000;export const Ref = /* */ 0b0000010000000;export const Snapshot = /* */ 0b0000100000000;export const Passive = /* */ 0b0001000000000;export const Hydrating = /* */ 0b0010000000000;export const HydratingAndUpdate = /* */ 0b0010000000100; export const NoEffect = /* */ 0b00000000;export const UnmountSnapshot = /* */ 0b00000010;export const UnmountMutation = /* */ 0b00000100;export const MountMutation = /* */ 0b00001000;export const UnmountLayout = /* */ 0b00010000;export const MountLayout = /* */ 0b00100000;export const MountPassive = /* */ 0b01000000;export const UnmountPassive = /* */ 0b10000000;
這麼設計是為了簡化類型比較與類型複合,如果項目開發的過程中有過一些複合許可權系統的設計經驗,那麼可能第一眼就能反應過來,所以 UnmountPassive|MountPassive
就是 0b11000000。如果對應的位為非零,則表示 tag 實現了指定的行為。這個在未來會用到,我們這裡先不涉及,所以就先放在這裡了解即可。
3.2 mountEffectImpl & updateEffectImpl
接著我們來看看 mountEffectImpl
與 updateEffectImpl
的具體實現。
3.2.1 mountEffectImpl
首先是 mountEffectImpl
:
function mountEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void { const hook = mountWorkInProgressHook(); // 創建一個新的 Hook 並返回當前 workInProgressHook const nextDeps = deps === undefined ? null : deps; sideEffectTag |= fiberEffectTag; hook.memoizedState = pushEffect(hookEffectTag, create, undefined, nextDeps);}
mountWorkInProgressHook
我們在第 3 篇 4.3.3: mountWorkInProgressHook 中解析過,其就是創建一個新的 Hook 並返回當前 workInProgressHook,具體原理不再贅述。
sideEffectTag
是按位或上 fiberEffectTag
然後賦值,在 renderWithHooks
中掛載在 renderedWork.effectTag
上,並在每次渲染後重置為 0。
renderedWork.effectTag |= sideEffectTag;sideEffectTag = 0;
具體 renderedWork.effectTag
有什麼用,我們後續會說到。
renderWithHooks 在 第 3 篇 4.3.1: renderWithHooks 中解析過,此處不再贅述。
hook.memoizedState
記錄 pushEffect
的返回結果,這個同記錄 useState 中的 newState 的原理是一致的。那麼現在的重點轉移到了 pushEffect
究竟做了什麼。
3.3.2 updateEffectImpl
接下來我們看看 updateEffectImpl
又做了些什麼工作呢?
function updateEffectImpl(fiberEffectTag, hookEffectTag, create, deps): void { const hook = updateWorkInProgressHook(); // 獲取當前正在工作中的 Hook 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(NoHookEffect, create, destroy, nextDeps); return; } } } sideEffectTag |= fiberEffectTag; hook.memoizedState = pushEffect(hookEffectTag, create, destroy, nextDeps);}
updateWorkInProgressHook
我們在第 3 篇 4.4.3: updateWorkInProgressHook 中解析過,其就是獲取當前正在工作中的 Hook,具體原理不再贅述。
可以發現在 currentHook
為空的時候, updateEffectImpl
的邏輯與 mountEffectImpl
的邏輯是一模一樣的;當 currentHook
不為空的時候, pushEffect
的第三個參數不是 undefined,而是 destroy。並且,在這個分支存在 areHookInputsEqual(nextDeps,prevDeps)
,即如果當前 useEffect 的 deps 和上一階段的 useEffect 的 deps 相等( areHookInputsEqual
所做的事情就是遍歷比較兩個 deps 是否相等,這裡就不展開解讀了),那就執行 pushEffect(NoHookEffect,create,destroy,nextDeps);
,大膽猜測 NoHookEffect
的意思就是不執行這次的 useEffect。如此,這段程式碼的邏輯就和我們之前自己實現的 useEffect 是一致的。
根據 第 3 篇 4.4.3: updateWorkInProgressHook,我們得知 currentHook
就是當前階段正在處理的 Hook,其正常邏輯下不會為空。那我們接下來需要重點關注的應該是 pushEffect
做了什麼,其第三個參數有什麼含義?
3.3 pushEffect
function pushEffect(tag, create, destroy, deps) { // 聲明一個新的 effect const effect: Effect = { tag, create, destroy, deps, // Circular next: (null: any), // 函數組件中定義的下一個 effect 的引用 }; if (componentUpdateQueue === null) { componentUpdateQueue = createFunctionComponentUpdateQueue(); // 初始化 componentUpdateQueue 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;}
type Effect = { tag: HookEffectTag, // 一個二進位數,它將決定 effect 的行為 create: () => (() => void) | void, // 繪製後應該運行的回調 destroy: (() => void) | void, // 用於確定是否應銷毀和重新創建 effect deps: Array<mixed> | null, // 決定重繪製後是否執行的 deps next: Effect, // 函數組件中定義的下一個 effect 的引用};
這個函數首先根據入參聲明了一個新的 effect,數據結構也給出來了,它同樣也是一個循環鏈表。tag 是
接下來根據 componentUpdateQueue 是否為空走兩套邏輯,而 componentUpdateQueue 的結構其實很簡單:
export type FunctionComponentUpdateQueue = { lastEffect: Effect | null,};
可見,componentUpdateQueue 其實就是一個存儲 Effect 的全局變數。
- componentUpdateQueue 為空:這種情況就是 mountEffect 時候的邏輯,它會創建一個空的 componentUpdateQueue,它其實只是
{lastEffect:null}
,之後將componentUpdateQueue.lastEffect
指向effect.next
,其實就是存了一下 effect。 - componentUpdateQueue 不為空:這種情況就是 updateEffect 時候會走到的邏輯
- lastEffect 為空:這種情況是新的渲染階段的第一個 useEffect,邏輯處理和 componentUpdateQueue 為空時一致。
- lastEffect 不為空:這種情況意味著這個組件有多個 useEffect,是第二個及其之後的 useEffect 會走到的分支,將 lastEffect 指向下一個 effect。
最後 return 一個 effect。
3.4 React Fiber 流程分析
看似源碼到這裡就結束了,但我們還存留幾個問題沒有解決:
effect.tag
的那些二進位數是什麼意思?pushEffect
之後還有什麼邏輯?componentUpdateQueue
存儲 Effect 之後會在哪裡被用到?
在 renderWithHooks
中, componentUpdateQueue
會被賦值到 renderedWork.updateQueue
上,包括我們 3.2 中的 sideEffectTag
也會賦值到 renderedWork.effectTag
上。
renderedWork.updateQueue = (componentUpdateQueue: any);renderedWork.effectTag |= sideEffectTag;
在第 3 篇 4.3.1: renderWithHooks中,我們分析出 renderWithHooks 是在函數組件更新階段( updateFunctionComponent
)執行的函數,這裡我們要想知道上面三個問題的答案,必須要把整個 Reconciler 的流程走一遍才能解析清楚。我個人認為 Fiber 是 React 16 中最複雜的一塊邏輯了,所以在前面幾篇中我只是略微提及,並沒有展開篇幅解析。Fiber 裡面的內容很多,如果展開的話足夠寫幾篇文章了,因此這裡也盡量簡單快捷的走一遍流程,忽略本文不相關的細節,只梳理部分邏輯的實現,重點關注我們調用 useEffect 之後的邏輯。
註:如果對這部分不感興趣的同學可以直接跳到 3.5 繼續閱讀。
React Fiber 優秀的文章有很多,這裡再推薦閱讀幾篇文章和影片來幫助有興趣的同學來了解
- A Cartoon Intro to Fiber – React Conf 2017
- React Fiber初探
- 這可能是最通俗的 React Fiber 打開方式
那我們開始吧!
3.4.1 ReactDOM.js
頁面渲染的唯一入口便是 ReactDOM.render,
ReactRoot.prototype.render = ReactSyncRoot.prototype.render = function( children: ReactNodeList, callback: ?() => mixed,): Work { // ... 忽略無關程式碼 updateContainer(children, root, null, work._onCommit); return work;};
render 的核心是調用 updateContainer
,這個函數來自於 react-reconciler 中的 ReactFiberReconciler.js。
3.4.2 ReactFiberReconciler.js
這個文件其實也是 react-reconciler 的入口,我們先看看 updateContainer
究竟是什麼:
export function updateContainer( element: ReactNodeList, container: OpaqueRoot, parentComponent: ?React$Component<any, any>, callback: ?Function,): ExpirationTime { // ... 忽略無關程式碼 return updateContainerAtExpirationTime( element, container, parentComponent, expirationTime, suspenseConfig, callback, );}
忽略無關程式碼發現它其實只是 updateContainerAtExpirationTime
的一層封裝,那我們看看這個是什麼:
export function updateContainerAtExpirationTime( element: ReactNodeList, container: OpaqueRoot, parentComponent: ?React$Component<any, any>, expirationTime: ExpirationTime, suspenseConfig: null | SuspenseConfig, callback: ?Function,) { // ... 忽略無關程式碼 return scheduleRootUpdate( current, element, expirationTime, suspenseConfig, callback, );}
再次忽略一些無關程式碼,發現它又是 scheduleRootUpdate
的一層封裝……那我們再看看 scheduleRootUpdate
是什麼:
function scheduleRootUpdate( current: Fiber, element: ReactNodeList, expirationTime: ExpirationTime, suspenseConfig: null | SuspenseConfig, callback: ?Function,) { // ... 忽略無關程式碼 enqueueUpdate(current, update); scheduleWork(current, expirationTime); return expirationTime;}
忽略一小段無關程式碼,發現它的核心是做兩件事, enqueueUpdate
我們這裡暫時先不管,重點看看任務調度 scheduleWork
,它相當於是 Fiber 邏輯的入口了,在 ReactFiberWorkLoop.js 中定義。
3.4.3 ReactFiberWorkLoop.js – render
ReactFiberWorkLoop.js 的內容非常長,有 2900 行程式碼,是包含任務循環主邏輯,不過我們剛才弄清楚要從 scheduleWork
開始著手那就慢慢梳理:
export function scheduleUpdateOnFiber( fiber: Fiber, expirationTime: ExpirationTime,) { // ... 忽略無關程式碼 const priorityLevel = getCurrentPriorityLevel(); if (expirationTime === Sync) { if ( (executionContext & LegacyUnbatchedContext) !== NoContext && (executionContext & (RenderContext | CommitContext)) === NoContext ) { schedulePendingInteractions(root, expirationTime); let callback = renderRoot(root, Sync, true); while (callback !== null) { callback = callback(true); } } else { scheduleCallbackForRoot(root, ImmediatePriority, Sync); if (executionContext === NoContext) { flushSyncCallbackQueue(); } } } else { scheduleCallbackForRoot(root, priorityLevel, expirationTime); } // ... 忽略特殊情況的處理}export const scheduleWork = scheduleUpdateOnFiber;
其實這段程式碼大部分分支都會收回到 renderRoot
上,再對 renderRoot
的回調做 while 循環處理。所以我們與其說 scheduleWork
是 Fiber 邏輯的入口,不如說 renderRoot
是入口。renderRoot
就是大名鼎鼎的 Fiber 兩個階段中的 render 階段。

圖源 A Cartoon Intro to Fiber – React Conf 2017
其實 debug 一下也容易看出這兩個階段:

renderRoot
中的程式碼也非常複雜,我們重點關注和本文有關的邏輯:
function renderRoot( root: FiberRoot, expirationTime: ExpirationTime, isSync: boolean,): SchedulerCallback | null { if (isSync && root.finishedExpirationTime === expirationTime) { // There's already a pending commit at this expiration time. return commitRoot.bind(null, root); // 進入 commit 階段 } // ... do { try { if (isSync) { workLoopSync(); } else { workLoop(); // 核心邏輯 } break; } catch (thrownValue) { // ... } while (true); // ...}
把一些多餘的程式碼略去之後,我們關注到兩個重要的點:
workLoop
是程式碼的核心部分,配合循環來實現任務循環。- 在超時的情況下,會進入 commit 階段。
我們先看看 workLoop
的邏輯:
function workLoop() { while (workInProgress !== null && !shouldYield()) { workInProgress = performUnitOfWork(workInProgress); }}
看來我們重點是需要看看 performUnitOfWork
:
function performUnitOfWork(unitOfWork: Fiber): Fiber | null { const current = unitOfWork.alternate; // ... 忽略計時邏輯 let next; if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) { next = beginWork(current, unitOfWork, renderExpirationTime); } else { next = beginWork(current, unitOfWork, renderExpirationTime); } // ... 忽略特殊邏輯 ReactCurrentOwner.current = null; return next;}
我們忽略計時邏輯,發現這段程式碼的內容其實就是兩個 beginWork
(這裡解答了我們在第3篇中 4.3.1 中留下的問題)。這個 beginWork
引自 ReactFiberBeginWork.js。
3.4.4 ReactFiberBeginWork.js
本節程式碼分析同 第 3 篇 4.3.1: renderWithHooks,不再贅述。
也就是現在我們 renderedWork
上的 updateQueue
(還記得它嗎?它的內容是 Effect 鏈表) 和 effectTag
掛到了 Fiber 上,跳過這部分邏輯,我們看看 Fiber 最後怎麼處理它們。
3.4.5 ReactFiberWorkLoop.js – commit
在剛才分析 renderRoot
的過程中,我們關注到任務超時之後會直接進入 commit 階段。我們先看看 commitRoot
的程式碼:
function commitRoot(root) { const renderPriorityLevel = getCurrentPriorityLevel(); runWithPriority( ImmediatePriority, commitRootImpl.bind(null, root, renderPriorityLevel), ); return null;}
好的,這裡發現我們應該關注 commitRootImpl
,來看看:
function commitRootImpl(root, renderPriorityLevel) { // ... startCommitTimer(); // Get the list of effects. let firstEffect; if (finishedWork.effectTag > PerformedWork) { if (finishedWork.lastEffect !== null) { finishedWork.lastEffect.nextEffect = finishedWork; firstEffect = finishedWork.firstEffect; } else { firstEffect = finishedWork; } } else { firstEffect = finishedWork.firstEffect; } if (firstEffect !== null) { do { try { commitBeforeMutationEffects(); } catch (error) { invariant(nextEffect !== null, 'Should be working on an effect.'); captureCommitPhaseError(nextEffect, error); nextEffect = nextEffect.nextEffect; } } while (nextEffect !== null); stopCommitSnapshotEffectsTimer(); if (enableProfilerTimer) { // Mark the current commit time to be shared by all Profilers in this // batch. This enables them to be grouped later. recordCommitTime(); } // The next phase is the mutation phase, where we mutate the host tree. startCommitHostEffectsTimer(); nextEffect = firstEffect; do { try { commitMutationEffects(root, renderPriorityLevel); } catch (error) { invariant(nextEffect !== null, 'Should be working on an effect.'); captureCommitPhaseError(nextEffect, error); nextEffect = nextEffect.nextEffect; } } while (nextEffect !== null); stopCommitHostEffectsTimer(); resetAfterCommit(root.containerInfo); // The work-in-progress tree is now the current tree. This must come after // the mutation phase, so that the previous tree is still current during // componentWillUnmount, but before the layout phase, so that the finished // work is current during componentDidMount/Update. root.current = finishedWork; // The next phase is the layout phase, where we call effects that read // the host tree after it's been mutated. The idiomatic use case for this is // layout, but class component lifecycles also fire here for legacy reasons. startCommitLifeCyclesTimer(); nextEffect = firstEffect; do { try { commitLayoutEffects(root, expirationTime); } catch (error) { invariant(nextEffect !== null, 'Should be working on an effect.'); captureCommitPhaseError(nextEffect, error); nextEffect = nextEffect.nextEffect; } } while (nextEffect !== null); stopCommitLifeCyclesTimer(); nextEffect = null; // Tell Scheduler to yield at the end of the frame, so the browser has an // opportunity to paint. requestPaint(); if (enableSchedulerTracing) { __interactionsRef.current = ((prevInteractions: any): Set<Interaction>); } executionContext = prevExecutionContext; } else { // No effects. // ... } stopCommitTimer(); nextEffect = firstEffect; while (nextEffect !== null) { const nextNextEffect = nextEffect.nextEffect; nextEffect.nextEffect = null; nextEffect = nextNextEffect; } // ... return null;}
commitRootImpl
的程式碼是真的很長,我這裡忽略了一些和 effect 處理無關的程式碼,剩下我們閱讀一下,發現當 effect 存在的時候,有三段邏輯要處理,它們的邏輯基本相同,循環 effect 鏈表傳給三個不同的函數,分別是:
- commitBeforeMutationEffects
- commitMutationEffects
- commitLayoutEffects
最後將循環 effect,將 nextEffect 賦值成 nextNextEffect。
限於篇幅問題,且第三個函數關於 useLayoutEffect,所以左右這裡這三個函數我們這裡都不一一展開解釋了,留給下篇文章中分析 useLayoutEffect 再來詳解。所以 3.4 中我們留下的問題—— effect.tag
的那些二進位數是什麼意思?這個問題也需要等到下一篇文章中來解釋了。
我們這裡只需要知道這三個函數的核心程式碼分別引用了 ReactFiberCommitWork.js 中的 commitWork
, commitBeforeMutationLifeCycles
, commitLifeCycles
,而這三個函數的核心程式碼在處理 FunctionCompoment 的邏輯時都走到了 commitHookEffectList
中即可。
3.5 commitHookEffectList
分析了一大圈,最後我們看看 ReactFiberCommitWork.js 中 commitHookEffectList
的邏輯,這裡便是 useEffect 終點了:
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); }}
可以發現,這裡的程式碼很清楚,這裡把 renderedWork.updateQueue
上的 effect 取了下來,在 unmount 的時候執行 effect.destory
(也就是 useEffect 第一個參數的返回值),在 mount 的時候執行 effect.create
(也就是 useEffect 傳入的第一個參數)。並且,循環所有的 effect 直到結束。
同時這裡也印證了我們之前的猜想:當 tag 是 NoHookEffect
的時候什麼也不做。
這裡我們把 useEffect 的源碼解釋清楚了,但是遺留了一個問題:effect.tag
這個參數究竟有什麼用?目前我們僅僅知道當它是 NoHookEffect
時的作用是不執行 useEffect 的內容,但是其他的值我們還沒有分析到,它們分析邏輯主要在我們 3.4.5 略過的那三個函數里。在下篇文章中,我們分析 useLayoutEffect 中會拿出來詳細分析。
大家再見。
最後附上 3.4 節分析的流程圖:
