【React 源码阅读】useEffect 和 useLayoutEffect 的区别

68 阅读3分钟

1、前言

useEffectuseLayoutEffect 在日常开发中经常会用到,但是你确定能够分得清楚这两个 hook 的差别吗?这篇文章会从源码层面,带你彻底搞清楚 useEffectuseLayoutEffect 的差别。

2、源码阅读

首先让我们来看一段经典的 React 代码:

import { createRoot } from 'react-dom/client';  

const domNode = document.getElementById('root');  
const root = createRoot(domNode);
root.render(<App />);

显然,React 在首次渲染的时候主要依赖这两个方法:

  • createRoot
  • render

2.1、createRoot

以下是梳理后的函数调用关系: image.png

最终拿到的 root 对象是这样的结构:

image.png

2.2、render

以下是梳理出来的 render 的函数调用关系:

image.png

2.2.1、renderWithHooks

renderWithHooks 是在 beginWork 阶段执行的,以下是梳理出来的 renderWithHooks 的函数调用逻辑:

image.png

注意这里的 ReactSharedInternals.H 实际上就是不同模式(mount / update)下的 hooks 对象。

const HooksDispatcherOnMount: Dispatcher = {
  readContext,

  use,
  useCallback: mountCallback,
  useContext: readContext,
  useEffect: mountEffect,
  useImperativeHandle: mountImperativeHandle,
  useLayoutEffect: mountLayoutEffect,
  useInsertionEffect: mountInsertionEffect,
  useMemo: mountMemo,
  useReducer: mountReducer,
  useRef: mountRef,
  useState: mountState,
  useDebugValue: mountDebugValue,
  useDeferredValue: mountDeferredValue,
  useTransition: mountTransition,
  useSyncExternalStore: mountSyncExternalStore,
  useId: mountId,
  useHostTransitionStatus: useHostTransitionStatus,
  useFormState: mountActionState,
  useActionState: mountActionState,
  useOptimistic: mountOptimistic,
  useMemoCache,
  useCacheRefresh: mountRefresh,
};

const HooksDispatcherOnUpdate: Dispatcher = {
  readContext,

  use,
  useCallback: updateCallback,
  useContext: readContext,
  useEffect: updateEffect,
  useImperativeHandle: updateImperativeHandle,
  useInsertionEffect: updateInsertionEffect,
  useLayoutEffect: updateLayoutEffect,
  useMemo: updateMemo,
  useReducer: updateReducer,
  useRef: updateRef,
  useState: updateState,
  useDebugValue: updateDebugValue,
  useDeferredValue: updateDeferredValue,
  useTransition: updateTransition,
  useSyncExternalStore: updateSyncExternalStore,
  useId: updateId,
  useHostTransitionStatus: useHostTransitionStatus,
  useFormState: updateActionState,
  useActionState: updateActionState,
  useOptimistic: updateOptimistic,
  useMemoCache,
  useCacheRefresh: updateRefresh,
};

很容易理解的是,在 mountupdate 阶段,React 内部调用的是不同模式的 hook
其中,useEffectuseLayoutEffect 的函数调用关系分别为:

image.png

image.png

只看图的话会发现,useEffectuseLayoutEffect 在底层实现这块,除了前面如 mountEffectmountLayoutEffect 的差别外,其他依赖的函数其实是一样的。
其实,这两个 hookrenderWithHooks 阶段都是不会执行的,那 React 内部是怎么区分和存储的呢?

2.2.2、React 如何存储以及区分不同的 hook

首先,对于这两个 hook 来说,它们分成了 mountupdate 模式,不同模式下执行的逻辑不同:

image.png

从上面梳理的行为来看,无论是 mount 还是 update 模式,我们定义的 useEffectuseLayoutEffect 在当前的 renderWithHooks 阶段都是不会执行的。

在这里我们也能得出一些结论:

React 如何存储 hook

  • 对于 mount 阶段的 hook,存在 workInProgressHook
  • 对于 update 阶段的 hook,每次 upadte 时会从 workInProgressHook 上按顺序取出 hook,对于出现 deps 变化时,将会维护一个 effect 链表,指向 fiber 上的 updateQueue 对象上。

我们尝试用图来表示这里不同节点之间的指向关系,应该会更加明了:

image.png

React 如何区分不同的 hook 呢? 答案就是,React 内部对于不同的 hook 会使用不同的 flag 来进行区分。比如,useEffect 使用 HookPassive 来标记,useLayoutEffect 则使用 HookLayout 来进行标记

2.2.2、commitRoot

以下是梳理出来的调用关系:

image.png

2.2.2.1、flushPassiveEffects

函数调用关系如下:

flushPassiveEffects.png

我们需要关注的是 commitHookEffectListUnmountcommitHookEffectListMount 这两个函数。

commitHookEffectListUnmount 中,执行的是 hook 中的 destroy 函数:

image.png

commitHookEffectListMount 中,主要是执行用户传入 hookcreate 函数,以及拿到 create 函数的返回值并赋值给 destroy 变量。

image.png

2.2.2.2、flushMutationEffects

这个函数的调用关系相对来说要简单一些:

image.png

其实和 useLayoutEffect 相关的很明显就是 commitHookEffectListUnmount 这个函数了:

image.png

显而易见,这里是在执行 useLayoutEffectdestroy 函数。

2.2.2.3、flushLayoutEffects

调用关系如下:

flushLayoutEffects.png

我们重点关注最后一个函数:commitHookEffectListMount

image.png

同样的,在这里也会执行 create 函数,并把它的返回值赋值给 destroy 函数

3、总结

3.1、相同点

  1. useEffectuseLayoutEffect 的用法相似,deps 变化后会重新执行,在传入的 create 函数内支持使用返回值作为 destroy 函数。
  2. useEffectuseLayoutEffect 一样都是存储在 fiber 指向的 workInProgressHook,出现 deps 变更后,effect 存储在 fiber 指向的 updateQueue
  3. useEffectuseLayoutEffect 都在 commit 阶段执行

3.2、差异点

  1. useEffect 通过 Scheduler 来进行调度,而 useLayoutEffectcommit 阶段会直接执行。换句话说,useEffect 是异步执行的,而 useLayoutEffect 是同步执行的。也可以说:useLayoutEffect 是在渲染之前执行的,而 useEffect 是在渲染之后执行的。