在 React 中,数据获取、设置订阅以及手动更改 React 组件中的 DOM 都属于副作用。在 React 16.8 新增了 Hook,这些副作用都可以通过 useEffect 或者 useLayoutEffect 来调用。前阵子抽空学了下 useState 的源码和做了总结,今天也稍微总结了下 use(Layout)Effect。
写在前面
不熟悉 Fiber 架构的话可以参考 https://blog.zhouweibin.top/detail/60aa954b6da89c0011cca404
对
useState
原理感兴趣的可以参考我之前写的博客 https://blog.zhouweibin.top/detail/60a16c423ba50e0011ffbc3b
涉及到上面知识的本文可能不会赘述,下面主要重点谈谈 use(Layout)Effect
这两个 Hooks 在 React 挂载和更新过程中做的事情
effectList
这是一个副作用链表,会连接具有副作用 effect
的 fiber 节点,副作用的数据格式如下
{
create: Function, // 传入use(Layout)Effect函数的第一个参数,即回调函数
destroy: Function, // 回调函数return的函数,在组件销毁的时候执行
deps: any[], // 依赖项
next: Effect, // 指向下一个effect
tag: string, // effect的类型,区分是useEffect还是useLayoutEffect
}
副作用在函数组件中体现为 use(Layout)Effect
的回调和销毁函数(即回调的返回值)(统称为副作用)
收集
从单个 fiber 节点看,fiber.memorizedState
挂载着 Hooks 链表,Hooks 链表会按顺序连接该节点上的 Hook,其中 use(Layout)Effect
上的 memorizedState
会存放副作用,待执行的副作用会串联成一个循环链表存储在updateQueue
上,如下图所示:
从整个 Fiber Tree 来看,render 阶段会自下而上创建 effectList,主要是通过completeUnitOfWork
这个方法创建,只有依赖项有变化才会将当前的 fiber 节点追加到 effectList。如下图所示:
最终的 effectList 如下所示:
首次挂载组件(mount),执行 useEffect
时,会调用mountEffectImpl
,简化后的源码如下:
function mountEffectImpl(fiberFlags, hookFlags, create, deps) {
const hook = mountWorkInProgressHook(); // 创建hook对象
const nextDeps = deps === undefined ? null : deps; // 依赖
// NOTE 给 fiber 节点打上副作用的 effectTag
currentlyRenderingFiber.flags = fiberFlags;
// 创建 effect 对象,挂载到 hook 的 memoizedState 上
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
undefined,
nextDeps
);
}
更新时(update),会调用 updateEffectImpl
function updateEffectImpl(fiberFlags, hookFlags, create, deps): void {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
let destroy = undefined;
if (currentHook !== null) {
const prevEffect = currentHook.memoizedState;
// 获取上一次 effect 的 destroy 函数
destroy = prevEffect.destroy;
if (nextDeps !== null) {
const prevDeps = prevEffect.deps;
// NOTE 如果前后依赖相同
if (areHookInputsEqual(nextDeps, prevDeps)) {
pushEffect(hookFlags, create, destroy, nextDeps);
return;
}
}
}
// NOTE flags的作用
currentlyRenderingFiber.flags |= fiberFlags;
// 如果前后依赖有变,在 effect 的 tag 中加入 HookHasEffect
// 并将新的 effect 挂载到 hook.memoizedState 上
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
destroy,
nextDeps
);
}
组件 mount 和 update 时收集 effect 的区别在于,前者无须调用前一次的销毁函数,后者在创建 effect 前会判断依赖项,主要依赖项变化才会创建 effect 并挂载到 hook 上
不管组件处于什么状态,都会调用到pushEffect
这个函数它的作用其实就是创建 effect 对象,将 effect 对象追加到 fiber 的 updateQueue
上
function pushEffect(tag, create, destroy, deps) {
const effect: Effect = {
tag,
create,
destroy,
deps,
next,
};
// NOTE 从 workInProgress 上取到 updateQueue(和 effectList 的区别)
let componentUpdateQueue = currentlyRenderingFiber.updateQueue;
if (componentUpdateQueue === null) {
// 如果 updateQueue 为空,把 effect 放到链表中,和它自己形成闭环
componentUpdateQueue = createFunctionComponentUpdateQueue();
currentlyRenderingFiber.updateQueue = componentUpdateQueue;
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;
}
在执行完 fiber 节点时,如果有更新 effect,会将该 fiber 节点追加到父节点的 effectList
function completeUnitOfWork(unitOfWork: Fiber): void {
let completedWork = unitOfWork;
do {
const current = completedWork.alternate;
const returnFiber = completedWork.return;
let next = completeWork(current, completedWork, subtreeRenderLanes);
// effect list构建
if (returnFiber !== null && (returnFiber.flags & Incomplete) === NoFlags) {
// 层层拷贝
if (returnFiber.firstEffect === null) {
returnFiber.firstEffect = completedWork.firstEffect;
}
if (completedWork.lastEffect !== null) {
// 说明当前节点是兄弟节点,子节点有effect,已经给returnFiber.lastEffect赋值过了
if (returnFiber.lastEffect !== null) {
// 连接兄弟节点的effect
returnFiber.lastEffect.nextEffect = completedWork.firstEffect;
}
returnFiber.lastEffect = completedWork.lastEffect;
}
const flags = completedWork.flags;
// NOTE: 该 fiber 节点有 effect
if (flags > PerformedWork) {
// 当前节点有effect连接上effect list
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = completedWork;
} else {
// returnFiber没有firstEffect的情况是第一次遇见有effect的节点
returnFiber.firstEffect = completedWork;
}
returnFiber.lastEffect = completedWork;
}
}
// 兄弟元素遍历再到返返回父级
const siblingFiber = completedWork.sibling;
if (siblingFiber !== null) {
workInProgress = siblingFiber;
return;
}
completedWork = returnFiber;
workInProgress = completedWork;
} while (completedWork !== null);
}
执行
render 阶段 useEffect 和 useLayoutEffect 没什么区别,在 commit 阶段会有比较明显的区别。先看下 useEffect,主要有三个地方会执行
useEffect
- 刚进入 commit 阶段
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) {
// NOTE 记录有副作用的 effect
rootWithPendingPassiveEffects = root;
}
}
beforeMutation
,该阶段会异步调度 useEffect
function commitBeforeMutationEffects() {
while (nextEffect !== null) {
// ...
if ((flags & Passive) !== NoFlags) {
// 如果fiber节点上的flags存在Passive调度useEffect
if (!rootDoesHavePassiveEffects) {
// 锁住调度状态,防止多次调度
rootDoesHavePassiveEffects = true;
// NOTE NormalSchedulerPriority 一般优先级
scheduleCallback(NormalSchedulerPriority, () => {
flushPassiveEffects();
return null;
});
}
}
nextEffect = nextEffect.nextEffect;
}
}
- layout 阶段填充 effect 执行数组,真正执行 useEffect 的时候,实际上是先执行上一次 effect 的销毁函数,再执行本次 effect 的创建函数
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 才可以根据它的依赖变化情况来决定是否执行回调。
function schedulePassiveEffects(finishedWork: Fiber) {
// 获取到函数组件的 updateQueue
const updateQueue = finishedWork.updateQueue;
// 获取 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 时
// 分别插入销毁函数数组和创建函数数组
enqueuePendingPassiveHookEffectUnmount(finishedWork, effect);
enqueuePendingPassiveHookEffectMount(finishedWork, effect);
}
effect = next;
} while (effect !== firstEffect);
}
}
上边已经填充好 effect 执行数组了,接下来就是执行这个数组。执行过程是先循环待销毁的 effect 数组,再循环待创建的 effect 数组,这一过程发生在flushPassiveEffectsImpl
函数中
function flushPassiveEffectsImpl() {
// 先校验,如果root上没有 Passive effectTag 的节点,则直接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) {
const updateQueue = finishedWork.updateQueue;
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 = finishedWork.updateQueue;
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
的使用和区别
useEffect
在实现上利用 scheduler(关于 scheduler 后续会写篇文章介绍) 的异步调度函数:scheduleCallback
,这函数会将执行 useEffect 的动作作为一个任务去调度,这个任务会异步调用,发生在 DOM 渲染之后
而useLayoutEffect
发生在 DOM 渲染之前,执行时机和componentDidMount
、componentDidUpdate
是一致的,也就是说如果在useEffect
中触发状态更新或者 DOM 渲染,会额外追加一次 DOM 渲染
一般情况下建议使用 useEffect
,因为它 不会阻塞渲染,useLayoutEffect
会阻塞渲染,只有在涉及到修改 DOM、动画等场景下考虑使用 useLayoutEffect
- 不同
useEffect
回调触发了多次setState
,会触发多次 render 吗?
不会。setState 最后其实都会调用 scheduleUpdateOnFiber
// scheduleUpdateOnFiber
// ...
if (executionContext === NoContext) {
// Flush the synchronous work now, unless we're already working or inside
// a batch. This is intentionally inside scheduleUpdateOnFiber instead of
// scheduleCallbackForFiber to preserve the ability to schedule a callback
// without immediately flushing it. We only do this for user-initiated
// updates, to preserve historical behavior of legacy mode.
flushSyncCallbackQueue();
}
// ...
只有当 executionContext
为 NoContext
才会触发同步更新,其余情况都是批量更新。在 commit 的 beforeMutation 阶段,会赋值 executionContext
为 CommitContext
,代表进入 commit 阶段
// beforeMutation
// 将当前上下文标记为CommitContext,作为commit阶段的标志
// ...
const prevExecutionContext = executionContext;
executionContext |= CommitContext;
// ...