Airing

Airing

哲学系学生 / 小学教师 / 程序员,个人网站: ursb.me
github
email
zhihu
medium
tg_channel
twitter_id

React Hooks 源碼解析(4):useEffect

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 階段。

image

圖源 A Cartoon Intro to Fiber - React Conf 2017

其實 debug 一下也容易看出這兩個階段:

image

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 這個參數究竟有什麼用?目前我們僅僅知道當它是 NoHookEffect 的時候的作用是不執行 useEffect 的內容,但是其他的值我們還沒有分析到,它們分析邏輯主要在我們 3.4.5 略過的那三個函數裡。在下篇文章中,我們分析 useLayoutEffect 中會拿出來詳細分析。

大家再見。

最後附上 3.4 節分析的流程圖:

image

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。