1、前言
useEffect 和 useLayoutEffect 在日常开发中经常会用到,但是你确定能够分得清楚这两个 hook 的差别吗?这篇文章会从源码层面,带你彻底搞清楚 useEffect 和 useLayoutEffect 的差别。
2、源码阅读
首先让我们来看一段经典的 React 代码:
import { createRoot } from 'react-dom/client';
const domNode = document.getElementById('root');
const root = createRoot(domNode);
root.render(<App />);
显然,React 在首次渲染的时候主要依赖这两个方法:
createRootrender
2.1、createRoot
以下是梳理后的函数调用关系:
最终拿到的 root 对象是这样的结构:
2.2、render
以下是梳理出来的 render 的函数调用关系:
2.2.1、renderWithHooks
renderWithHooks 是在 beginWork 阶段执行的,以下是梳理出来的 renderWithHooks 的函数调用逻辑:
注意这里的 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,
};
很容易理解的是,在 mount 和 update 阶段,React 内部调用的是不同模式的 hook。
其中,useEffect 和 useLayoutEffect 的函数调用关系分别为:
只看图的话会发现,useEffect 和 useLayoutEffect 在底层实现这块,除了前面如 mountEffect 和 mountLayoutEffect 的差别外,其他依赖的函数其实是一样的。
其实,这两个 hook 在 renderWithHooks 阶段都是不会执行的,那 React 内部是怎么区分和存储的呢?
2.2.2、React 如何存储以及区分不同的 hook
首先,对于这两个 hook 来说,它们分成了 mount 和 update 模式,不同模式下执行的逻辑不同:
从上面梳理的行为来看,无论是 mount 还是 update 模式,我们定义的 useEffect 和 useLayoutEffect 在当前的 renderWithHooks 阶段都是不会执行的。
在这里我们也能得出一些结论:
React 如何存储 hook:
- 对于
mount阶段的hook,存在workInProgressHook上 - 对于
update阶段的hook,每次upadte时会从workInProgressHook上按顺序取出hook,对于出现deps变化时,将会维护一个effect链表,指向fiber上的updateQueue对象上。
我们尝试用图来表示这里不同节点之间的指向关系,应该会更加明了:
React 如何区分不同的 hook 呢?
答案就是,React 内部对于不同的 hook 会使用不同的 flag 来进行区分。比如,useEffect 使用 HookPassive 来标记,useLayoutEffect 则使用 HookLayout 来进行标记
2.2.2、commitRoot
以下是梳理出来的调用关系:
2.2.2.1、flushPassiveEffects
函数调用关系如下:
我们需要关注的是 commitHookEffectListUnmount 和 commitHookEffectListMount 这两个函数。
在 commitHookEffectListUnmount 中,执行的是 hook 中的 destroy 函数:
在 commitHookEffectListMount 中,主要是执行用户传入 hook 的 create 函数,以及拿到 create 函数的返回值并赋值给 destroy 变量。
2.2.2.2、flushMutationEffects
这个函数的调用关系相对来说要简单一些:
其实和 useLayoutEffect 相关的很明显就是 commitHookEffectListUnmount 这个函数了:
显而易见,这里是在执行 useLayoutEffect 的 destroy 函数。
2.2.2.3、flushLayoutEffects
调用关系如下:
我们重点关注最后一个函数:commitHookEffectListMount:
同样的,在这里也会执行 create 函数,并把它的返回值赋值给 destroy 函数
3、总结
3.1、相同点
useEffect和useLayoutEffect的用法相似,deps变化后会重新执行,在传入的 create 函数内支持使用返回值作为destroy函数。useEffect和useLayoutEffect一样都是存储在fiber指向的workInProgressHook,出现 deps 变更后,effect存储在fiber指向的updateQueue。useEffect和useLayoutEffect都在commit阶段执行
3.2、差异点
useEffect通过Scheduler来进行调度,而useLayoutEffect在commit阶段会直接执行。换句话说,useEffect是异步执行的,而useLayoutEffect是同步执行的。也可以说:useLayoutEffect是在渲染之前执行的,而useEffect是在渲染之后执行的。