React Hooks 源碼解析(4):useEffect

  • 2019 年 12 月 24 日
  • 筆記
  • React 源碼版本: v16.11.0
  • 源碼注釋筆記:airingursb/react

1. useEffect 簡介

1.1 為什麼要有 useEffect

我們在前文中說到 React Hooks 使得 Functional Component 擁有 Class Component 的特性,其主要動機包括:

  1. 在組件之間復用狀態邏輯很難
  2. 複雜組件變得難以理解
  3. 難以理解的 class

對於第二點,首先,針對 Class Component 來說,我們寫 React 應用時經常要在組件的各種生命周期中編寫程式碼,如在 componentDidMountcomponentDidUpdate 中發送 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|PassiveEffectUnmountPassive|MountPassive 究竟是什麼?

閱讀程式碼可知他們是從 ReactSideEffectTagsReactHookEffectTags 中引入的。

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

接著我們來看看 mountEffectImplupdateEffectImpl 的具體實現。

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 的全局變數。

  1. componentUpdateQueue 為空:這種情況就是 mountEffect 時候的邏輯,它會創建一個空的 componentUpdateQueue,它其實只是 {lastEffect:null},之後將 componentUpdateQueue.lastEffect 指向 effect.next,其實就是存了一下 effect。
  2. componentUpdateQueue 不為空:這種情況就是 updateEffect 時候會走到的邏輯
  3. lastEffect 為空:這種情況是新的渲染階段的第一個 useEffect,邏輯處理和 componentUpdateQueue 為空時一致。
  4. lastEffect 不為空:這種情況意味著這個組件有多個 useEffect,是第二個及其之後的 useEffect 會走到的分支,將 lastEffect 指向下一個 effect。

最後 return 一個 effect。

3.4 React Fiber 流程分析

看似源碼到這裡就結束了,但我們還存留幾個問題沒有解決:

  1. effect.tag 的那些二進位數是什麼意思?
  2. pushEffect 之後還有什麼邏輯?
  3. 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 優秀的文章有很多,這裡再推薦閱讀幾篇文章和影片來幫助有興趣的同學來了解

  1. A Cartoon Intro to Fiber – React Conf 2017
  2. React Fiber初探
  3. 這可能是最通俗的 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);  // ...}

把一些多餘的程式碼略去之後,我們關注到兩個重要的點:

  1. workLoop 是程式碼的核心部分,配合循環來實現任務循環。
  2. 在超時的情況下,會進入 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 中的 commitWorkcommitBeforeMutationLifeCyclescommitLifeCycles,而這三個函數的核心程式碼在處理 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 節分析的流程圖: