useEffect和useLayoutEffect的原理与区别
共 9676字,需浏览 20分钟
·
2022-04-30 12:30
本文适合对React的use(Layout)Effect有疑惑的小伙伴阅读。
欢迎关注前端早茶,与广东靓仔共同进阶~
一、前言
二、正文
Effect的数据结构
对函数组件来说,其fiber上的memorizedState专门用来存储hooks链表,每一个hook对应链表中的每一个元素。
useEffect/useLayoutEffect产生的hook会放到fiber.memorizedState上,而它们调用后最终会生成一个effect对象,存储到它们对应hook的memoizedState中,与其他的effect连接成环形链表。
单个的effect对象包括以下几个属性:
create: 传入use(Layout)Effect函数的第一个参数,即回调函数 destroy: 回调函数return的函数,在该effect销毁的时候执行 deps: 依赖项 next: 指向下一个effect tag: effect的类型,区分是useEffect还是useLayoutEffect
单纯看effect对象中的字段,很容易和平时的用法联系起来。create函数即我们传入useEffect/useLayoutEffect的回调函数,而通过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>effectdiv>
}
挂载到它fiber上memoizedState
的hooks链表结构如下
例如useEffect hook上的memoizedState
存储了useEffect 的 effect对象(effect1),next指向useLayoutEffect的effect对象(effect2)。effect2的next又指回effect1.在下面的useLayoutEffect hook中,也是如此的结构。
effect除了保存在fiber.memoizedState
对应的hook中,还会保存在fiber的updateQueue中。
现在,我们知道,调用use(Layout)Effect,最后会产生effect链表,这个链表保存在两个地方:
fiber.memoizedState的hooks链表中,use(Layout)Effect对应hook元素的memoizedState中。
fiber.updateQueue中,本次更新的updateQueue,它会在本次更新的commit阶段中被处理。
流程概述
render阶段:函数组件开始渲染的时候,创建出对应的hook链表挂载到 workInProgress的 memoizedState
上,并创建effect链表,但是基于上次和本次依赖项的比较结果, 创建的effect是有差异的。 这一点暂且可以理解为:依赖项有变化,effect可以被处理,否则不会被处理。 commit阶段:异步调度useEffect,layout阶段同步处理useLayoutEffect的 effect。等到commit阶段完成,更新应用到页面上之后,开始处理useEffect 产生的effect。
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并不存在,执行创建函数前也就无需先销毁。
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的异步调度
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;
}
}
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的同步执行
mutation
阶段执行,后者在layout
阶段执行。与useEffect不同的是,它不用数组去存储销毁和创建函数,而是直接操作fiber.updateQueue
。// 调用卸载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);
}
}
文章转载于 https://www.cnblogs.com/cczlovexw/p/16172130.html
四、总结
useEffect和useLayoutEffect作为组件的副作用,本质上是一样的。共用一套结构来存储effect链表。整体流程上都是先在render阶段,生成effect,并将它们拼接成链表,存到fiber.updateQueue上,最终带到commit阶段被处理。
他们彼此的区别只是最终的执行时机不同,一个异步一个同步,这使得useEffect不会阻塞渲染,而useLayoutEffect会阻塞渲染。
五、最后
在看源码前,我们先去官方文档复习下框架设计理念、源码分层设计 阅读下框架官方开发人员写的相关文章 借助框架的调用栈来进行源码的阅读,通过这个执行流程,我们就完整的对源码进行了一个初步的了解 接下来再对源码执行过程中涉及的所有函数逻辑梳理一遍
关注我,一起携手进阶
欢迎关注前端早茶,与广东靓仔共同进阶~