点击进入 React 源码调试仓库。
React 在构建用户界面整体遵循函数式的编程理念,即固定的输出有固定的输入,尤其是在推出函数式组件之后,更加强化了组件纯函数的理念。但理论业务中编写的组件未免要产生申请数据、订阅事件、手动操作 DOM 这些副作用(effect),这样不免让函数组件变得不那么纯,于是 React 提供 use(Layout)Effect 的 hook,给开发者提供专门治理副作用的形式。
上面咱们会从 effect 的数据结构动手,梳理 use(Layout)Effect 在 render 和 commit 阶段的整体流程。
Effect 的数据结构
对于 hook 链表构造的基本概念我曾经总结过一篇文章:React hooks 的根底概念:hooks 链表。对函数组件来说,其 fiber 上的 memorizedState 专门用来存储 hooks 链表,每一个 hook 对应链表中的每一个元素。use(Layout)Effect 产生的 hook 会放到 fiber.memorizedState 上,而它们调用后最终会生成一个 effect 对象,存储到它们对应 hook 的 memoizedState 中,与其余的 effect 连接成环形链表。
单个的 effect 对象包含以下几个属性:
- create: 传入 use(Layout)Effect 函数的第一个参数,即回调函数
- destroy: 回调函数 return 的函数,在该 effect 销毁的时候执行
- deps: 依赖项
- next: 指向下一个 effect
- tag: effect 的类型,辨别是 useEffect 还是 useLayoutEffect
单纯看 effect 对象中的字段,很容易和平时的用法分割起来。create 函数即咱们传入 use(Layout)Effect 的回调函数,而通过 deps,能够管制 create 是否执行,如需革除 effect,则在 create 函数中 return 一个新函数(即 destroy)即可。
为了了解 effect 的数据结构,假如有如下组件:
const UseEffectExp = () => {const [ text, setText] = useState('hello')
useEffect(() => {console.log('effect1')
return () => {console.log('destory1');
}
})
useLayoutEffect(() => {console.log('effect2')
return () => {console.log('destory2');
}
})
return <div>effect</div>
}
挂载到它 fiber 上 memoizedState 的 hooks 链表构造如下
例如 useEffect hook 上的 memoizedState 存储了 useEffect 的 effect 对象(effect1),next 指向 useLayoutEffect 的 effect 对象(effect2)。effect2 的 next 又指回 effect1. 在上面的 useLayoutEffect hook 中,也是如此的构造。
fiber.memoizedState ---> useState hook
|
|
next
|
↓
useEffect hook
memoizedState: useEffect 的 effect 对象 ---> useLayoutEffect 的 effect 对象
| ↑__________________________________|
|
next
|
↓
useLayoutffect hook
memoizedState: useLayoutEffect 的 effect 对象 ---> useEffect 的 effect 对象
↑___________________________________|
effect 除了保留在 fiber.memoizedState 对应的 hook 中,还会保留在 fiber 的 updateQueue 中。
fiber.updateQueue ---> useLayoutEffect ----next----> useEffect
↑ |
|__________________________|
当初,咱们晓得,调用 use(Layout)Effect,最初会产生 effect 链表,这个链表保留在两个中央:
- fiber.memoizedState 的 hooks 链表中,use(Layout)Effect 对应 hook 元素的 memoizedState 中。
- fiber.updateQueue 中,本次更新的 updateQueue,它会在本次更新的 commit 阶段中被解决。
流程概述
基于下面的数据结构,对于 use(Layout)Effect 来说,React 做的事件就是
- render 阶段:函数组件开始渲染的时候,创立出对应的 hook 链表挂载到 workInProgress 的 memoizedState 上,并创立 effect 链表,然而基于上次和本次依赖项的比拟后果,
创立的 effect 是有差别的。这一点暂且能够了解为:依赖项有变动,effect 能够被解决,否则不会被解决。
- commit 阶段:异步调度 useEffect,layout 阶段同步解决 useLayoutEffect 的 effect。等到 commit 阶段实现,更新利用到页面上之后,开始解决 useEffect 产生的 effect。
第二点提到了一个重点,就是 useEffect 和 useLayoutEffect 的执行机会不一样,前者被异步调度,当页面渲染实现后再去执行,不会阻塞页面渲染。
后者是在 commit 阶段新的 DOM 筹备实现,但还未渲染到屏幕之前,同步执行。
实现细节
通过整体流程能够看出,effect 的整个过程波及到 render 阶段和 commit 阶段。render 阶段只创立 effect 链表,commit 阶段去解决这个链表。所有实现的细节都是在围绕 effect 链表。
render 阶段 - 创立 effect 链表
在理论的应用中,咱们调用的 use(Layout)Effect 函数,在挂载和更新的过程是不同的。
挂载时,调用的是mountEffectImpl
,它会为 use(Layout)Effect 这类 hook 创立一个 hook 对象,将 workInProgressHook 指向它,而后在这个 fiber 节点的 flag 中退出副作用相干的 effectTag。最初,会构建 effect 链表挂载到 fiber 的 updateQueue,并且也会在 hook 上的 memorizedState 挂载 effect。
function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {
// 创立 hook 对象
const hook = mountWorkInProgressHook();
// 获取依赖
const nextDeps = deps === undefined ? null : deps;
// 为 fiber 打上副作用的 effectTag
currentlyRenderingFiber.flags |= fiberFlags;
// 创立 effect 链表,挂载到 hook 的 memoizedState 上和 fiber 的 updateQueue
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
undefined,
nextDeps,
);
}
currentlyRenderingFiber 即 workInProgress 节点
更新时,调用updateEffectImpl
,实现 effect 链表的构建。这个过程中会依据前后依赖项是否变动,从而创立不同的 effect 对象。具体体现在 effect 的 tag 上,如果前后依赖未变,则 effect 的 tag 就赋值为传入的 hookFlags,否则,在 tag 中退出 HookHasEffect 标记位。正是因为这样,在解决 effect 链表时才能够只解决依赖变动的 effect,use(Layout)Effect 能够依据它的依赖变动状况来决定是否执行回调。
function updateEffectImpl(fiberFlags, hookFlags, create, deps): void {const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
let destroy = undefined;
if (currentHook !== null) {
// 从 currentHook 中获取上一次的 effect
const prevEffect = currentHook.memoizedState;
// 获取上一次 effect 的 destory 函数,也就是 useEffect 回调中 return 的函数
destroy = prevEffect.destroy;
if (nextDeps !== null) {
const prevDeps = prevEffect.deps;
// 比拟前后依赖,push 一个不带 HookHasEffect 的 effect
if (areHookInputsEqual(nextDeps, prevDeps)) {pushEffect(hookFlags, create, destroy, nextDeps);
return;
}
}
}
currentlyRenderingFiber.flags |= fiberFlags;
// 如果前后依赖有变,在 effect 的 tag 中退出 HookHasEffect
// 并将新的 effect 更新到 hook.memoizedState 上
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
destroy,
nextDeps,
);
}
在组件挂载和更新时,有一个区别,就是挂载期间调用 pushEffect 创立 effect 对象的时候并没有传 destroy 函数,而更新期间传了,这是因为每次 effect 执行时,都是先执行前一次的销毁函数,再执行新 effect 的创立函数。而挂载期间,上一次的 effect 并不存在,执行创立函数前也就无需先销毁。
挂载和更新,都调用了 pushEffect,它的职责很单纯,就是创立 effect 对象,构建 effect 链表,挂到 WIP 节点的 updateQueue 上。
function pushEffect(tag, create, destroy, deps) {
// 创立 effect 对象
const effect: Effect = {
tag,
create,
destroy,
deps,
// Circular
next: (null: any),
};
// 从 workInProgress 节点上获取到 updateQueue,为构建链表做筹备
let componentUpdateQueue: null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue: any);
if (componentUpdateQueue === null) {
// 如果 updateQueue 为空,把 effect 放到链表中,和它本人造成闭环
componentUpdateQueue = createFunctionComponentUpdateQueue();
// 将 updateQueue 赋值给 WIP 节点的 updateQueue,实现 effect 链表的挂载
currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
componentUpdateQueue.lastEffect = effect.next = effect;
} else {
// updateQueue 不为空,将 effect 接到链表的后边
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;
}
函数组件和类组件的 updateQueue 都是环状链表
以上,就是 effect 链表的构建过程。咱们能够看到,effect 对象创立进去最终会以两种模式放到两个中央:单个的 effect,放到 hook.memorizedState 上;环状的 effect 链表,放到 fiber 节点的 updateQueue 中。两者各有用处,前者的 effect 会作为上次更新的 effect,为本次创立 effect 对象提供参照(比照依赖项数组),后者的 effect 链表会作为最终被执行的主体,带到 commit 阶段解决。
commit 阶段 -effect 如何被解决
useEffect 和 useLayoutEffect,对它们的解决最终都落在解决 fiber.updateQueue 上,对前者来说,循环 updateQueue 时只解决蕴含 useEffect 这类 tag 的 effect,对后者来说,只解决蕴含 useLayoutEffect 这类 tag 的 effect,它们的处理过程都是先执行前一次更新时 effect 的销毁函数(destroy),再执行新 effect 的创立函数(create)。
以上是它们的处理过程在宏观上的共性,宏观上的区别次要体现在执行机会上。useEffect 是在 beforeMutation 或 layout 阶段异步调度,而后在本次的更新利用到屏幕上之后再执行,而 useLayoutEffect 是在 layout 阶段同步执行的。上面先剖析 useEffect 的处理过程。
useEffect 的异步调度
与 componentDidMount、componentDidUpdate 不同的是,在浏览器实现布局与绘制之后,传给 useEffect 的函数会提早调用。
这使得它实用于许多常见的副作用场景,比方设置订阅和事件处理等状况,因而不应在函数中执行阻塞浏览器更新屏幕的操作。
基于 useEffect 回调 提早调用(实际上就是异步调用) 的需要,在实现上利用 scheduler 的异步调度函数:scheduleCallback
,将执行 useEffect 的动作作为一个工作去调度,这个工作会异步调用。
commit 阶段和 useEffect 真正扯上关系的有三个中央:commit 阶段的开始、beforeMutation、layout,波及到异步调度的是前面两个。
function commitRootImpl(root, renderPriorityLevel) {
// 进入 commit 阶段,先执行一次之前未执行的 useEffect
do {flushPassiveEffects();
} while (rootWithPendingPassiveEffects !== null);
...
do {
try {
// beforeMutation 阶段的处理函数:commitBeforeMutationEffects 外部,// 异步调度 useEffect
commitBeforeMutationEffects();} catch (error) {...}
} while (nextEffect !== null);
...
const rootDidHavePassiveEffects = rootDoesHavePassiveEffects;
if (rootDoesHavePassiveEffects) {
// 重点,记录有副作用的 effect
rootWithPendingPassiveEffects = root;
}
}
这三个中央去执行或者调度 useEffect 有什么用意呢?咱们别离来看。
- commit 开始,先执行一下 useEffect:这和 useEffect 异步调度的特点无关,它以个别的优先级被调度,这就意味着一旦有更高优先级的工作进入到 commit 阶段,上一次工作的 useEffect 还没失去执行。所以在本次更新开始前,须要先将之前的 useEffect 都执行掉,以保障本次调度的 useEffect 都是本次更新产生的。
- beforeMutation 阶段异步调度 useEffect:这个是实打实地针对 effectList 上有副作用的节点,去异步调度 useEffect。
function commitBeforeMutationEffects() {while (nextEffect !== null) {
...
if ((flags & Passive) !== NoFlags) {
// 如果 fiber 节点上的 flags 存在 Passive 调度 useEffect
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
scheduleCallback(NormalSchedulerPriority, () => {flushPassiveEffects();
return null;
});
}
}
nextEffect = nextEffect.nextEffect;
}
}
因为 rootDoesHavePassiveEffects
的限度,只会发动一次 useEffect 调度,相当于用一把锁锁住调度状态,防止发动屡次调度。
- layout 阶段填充 effect 执行数组:真正 useEffect 执行的时候,实际上是先执行上一次 effect 的销毁,再执行本次 effect 的创立。React 用两个数组来别离存储销毁函数和
创立函数,这两个数组的填充就是在 layout 阶段,到时候循环开释执行两个数组中的函数即可。
function commitLifeCycles(
finishedRoot: FiberRoot,
current: Fiber | null,
finishedWork: Fiber,
committedLanes: Lanes,
): void {switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent:
case Block: {
...
// layout 阶段填充 effect 执行数组
schedulePassiveEffects(finishedWork);
return;
}
}
在调用 schedulePassiveEffects
填充 effect 执行数组时,有一个重要的中央就是只在蕴含 HasEffect 的 effectTag 的时候,才将 effect 放到数组内,这一点保障了依赖项有变动再去解决 effect。也就是:如果前后依赖未变,则 effect 的 tag 就赋值为传入的 hookFlags,否则,在 tag 中退出 HookHasEffect 标记位。正是因为这样,在解决 effect 链表时才能够只解决依赖变动的 effect,use(Layout)Effect 才能够依据它的依赖变动状况来决定是否执行回调。
schedulePassiveEffects 的实现:
function schedulePassiveEffects(finishedWork: Fiber) {
// 获取到函数组件的 updateQueue
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
// 获取 effect 链表
const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
// 循环 effect 链表
do {const {next, tag} = effect;
if ((tag & HookPassive) !== NoHookEffect &&
(tag & HookHasEffect) !== NoHookEffect
) {
// 当 effect 的 tag 含有 HookPassive 和 HookHasEffect 时,向数组中 push effect
enqueuePendingPassiveHookEffectUnmount(finishedWork, effect);
enqueuePendingPassiveHookEffectMount(finishedWork, effect);
}
effect = next;
} while (effect !== firstEffect);
}
}
在调用 enqueuePendingPassiveHookEffectUnmount
和enqueuePendingPassiveHookEffectMount
填充数组的时候,还会再异步调度一次 useEffect,但这与 beforeMutation 的调度是互斥的,一旦之前调度过,就不会再调度了,同样是 rootDoesHavePassiveEffects
起的作用。
执行 effect
此时咱们曾经晓得,effect 得以被解决是因为之前的调度以及 effect 数组的填充。当初到了最初的步骤,执行 effect 的 destroy 和 create。过程就是先循环待销毁的 effect 数组,再循环待创立的 effect 数组,这一过程产生在 flushPassiveEffectsImpl
函数中。循环的时候每个两项去 effect 是因为奇数项存储的是以后的 fiber。
function flushPassiveEffectsImpl() {
// 先校验,如果 root 上没有 Passive efectTag 的节点,则间接 return
if (rootWithPendingPassiveEffects === null) {return false;}
...
// 执行 effect 的销毁
const unmountEffects = pendingPassiveHookEffectsUnmount;
pendingPassiveHookEffectsUnmount = [];
for (let i = 0; i < unmountEffects.length; i += 2) {const effect = ((unmountEffects[i]: any): HookEffect);
const fiber = ((unmountEffects[i + 1]: any): Fiber);
const destroy = effect.destroy;
effect.destroy = undefined;
if (typeof destroy === 'function') {
try {destroy();
} catch (error) {captureCommitPhaseError(fiber, error);
}
}
}
// 再执行 effect 的创立
const mountEffects = pendingPassiveHookEffectsMount;
pendingPassiveHookEffectsMount = [];
for (let i = 0; i < mountEffects.length; i += 2) {const effect = ((mountEffects[i]: any): HookEffect);
const fiber = ((mountEffects[i + 1]: any): Fiber);
try {
const create = effect.create;
effect.destroy = create();} catch (error) {captureCommitPhaseError(fiber, error);
}
}
...
return true;
}
useLayoutEffect 的同步执行
useLayoutEffect 在执行的时候,也是先销毁,再创立。和 useEffect 不同的是这两者都是同步执行的,前者在 mutation 阶段执行,后者在 layout 阶段执行。
与 useEffect 不同的是,它不必数组去存储销毁和创立函数,而是间接操作 fiber.updateQueue。
卸载上一次的 effect,产生在 mutation 阶段
// 调用卸载 layout effect 的函数,传入 layout 无关的 effectTag 和阐明 effect 有变动的 effectTag:HookLayout | HookHasEffect
commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork);
function commitHookEffectListUnmount(tag: number, finishedWork: Fiber) {
// 获取 updateQueue
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
// 循环 updateQueue 上的 effect 链表
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {if ((effect.tag & tag) === tag) {
// 执行销毁
const destroy = effect.destroy;
effect.destroy = undefined;
if (destroy !== undefined) {destroy();
}
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
执行本次的 effect 创立,产生在 layout 阶段
// 调用创立 layout effect 的函数
commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
function commitHookEffectListMount(tag: number, finishedWork: Fiber) {const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {if ((effect.tag & tag) === tag) {
// 创立
const create = effect.create;
effect.destroy = create();}
effect = effect.next;
} while (effect !== firstEffect);
}
}
总结
useEffect 和 useLayoutEffect 作为组件的副作用,实质上是一样的。共用一套构造来存储 effect 链表。整体流程上都是先在 render 阶段,生成 effect,并将它们拼接成链表,存到 fiber.updateQueue 上,最终带到 commit 阶段被解决。他们彼此的区别只是最终的执行机会不同,一个异步一个同步,这使得 useEffect 不会阻塞渲染,而 useLayoutEffect 会阻塞渲染。
欢送扫码关注公众号,发现更多技术文章