前言
在react的官方文档上,useLayoutEffect有这么一段描述,它可以阻塞浏览器的绘制,实际情况下这个api我们可能用的比较少,但是对于一些关乎用户体验的场景,它的作用就变得非常大,它可以使错误绘制的一帧被掩盖,直到我们计算出正确的内容,再显示给用户。
接下来就话不多说,直接步入主题,它究竟是怎么做到的?
useLayoutEffect
源码分析
react
的仓库在facebook/react,而useLayoutEffect
的源码在packages/react-reconciler/src/ReactFiberHooks.js
中
在函数组件被调用的过程中,useLayoutEffect的hook
也会被调用,在组件挂载和组件更新的周期中,useLayoutEffect
对应着两个方法,即mountLayoutEffect和updateLayoutEffect
它们则分别调用了mountEffectImpl,updateEffectImpl
,这两个函数useEffect
也会用到,因此useLayoutEffect
在挂载时传了一个fiberFlags
用于区分
js
function mountLayoutEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
// 此处省略, 注意这里传了一个fiberFlags
//用于区分useEffect和useLayoutEffect
return mountEffectImpl(fiberFlags, HookLayout, create, deps);
}
function updateLayoutEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
return updateEffectImpl(UpdateEffect, HookLayout, create, deps);
}
mountEffectImpl 和 updateEffectImpl
mountEffectImpln
即挂载Effect
,它会创建一个hook
对象并添加到当前fiber的hooks
链表中,并且创建一个Effect
,并记录到hook
对象中,记录的东西自然也就是useLayoutEffect
的回调函数、依赖数组了
而updateEffectImpl
则是去浅比较前后的依赖数组中的变量是否有改变,如果有则改变更新它的回调函数,并打上标记,在后续的流程中触发它
js
function mountEffectImpl(
fiberFlags: Flags,
hookFlags: HookFlags,
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
currentlyRenderingFiber.flags |= fiberFlags;
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
createEffectInstance(),
nextDeps,
);
}
function updateEffectImpl(
fiberFlags: Flags,
hookFlags: HookFlags,
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const effect: Effect = hook.memoizedState;
const inst = effect.inst;
// currentHook is null on initial mount when rerendering after a render phase
// state update or for strict mode.
if (currentHook !== null) {
if (nextDeps !== null) {
const prevEffect: Effect = currentHook.memoizedState;
const prevDeps = prevEffect.deps;
if (areHookInputsEqual(nextDeps, prevDeps)) {
hook.memoizedState = pushEffect(hookFlags, create, inst, nextDeps);
return;
}
}
}
currentlyRenderingFiber.flags |= fiberFlags;
hook.memoizedState = pushEffect(
HookHasEffect | hookFlags,
create,
inst,
nextDeps,
);
}
看完useLayoutEffect
的基本源码,接下来我们看看它的调用时机
useLayoutEffect回调函数的调用时机
react
中有一个关键的概念,就是fiber
架构,而fiber
架构的核心也就是调度器了,16.8以后,react中几乎所有的任务都通过调度器来进行调度,useLayoutEffect
的回调也不例外,在分析其回调的调用时机之前,还需先讲一下react
的调度器调度任务的本质
react调度任务时会根据优先级分别调用两个api,也就是scheduleImmediateTask
和scheduleCallback
,
scheduleImmediateTask
会通过微任务进行调度任务(microtask 或 promise
),而scheduleCallback
则通过宏任务进行调度(setImediate、messageChannel和setTimeout
),这样调度任务正好与浏览器的绘制时机撞上了
浏览器在一帧的绘制中会执行以下的任务,其中js
的任务执行,浏览器会保证同步任务和微任务都得到执行完,而宏任务如果时间不够则会放到下一帧进行执行,也因此,react
大量的使用宏任务去调度任务,一定程度上避免了阻塞浏览器的绘制(更多的还是依赖时间切片完成的)
那么
useLayoutEffect
的回调的调度不言而喻也就是微任务调度的了,现在我们来找找它的调度时机
在函数组件一次的更新中,会先通过调度器调度fiber
节点上的所有任务,再根据是否要进行时间切片,进入协调阶段,更新fiber
树,当所有fiber
节点协调阶段的递与归阶段都执行完了以后,就到了react提交阶段,这个也就是useLayoutEffect和useEffect
执行时机不同的地方
react的提交阶段也分为三个阶段,分别对应着commitBeforeMutationEffects、commitEffects和commitLayoutEffects
这三个方法的调用
它会在commitBeforeMutationEffects方法之前通过宏任务调度flushPassiveEffects
方法来useEffect的回调和销毁函数
js
function commitRootImpl(
root: FiberRoot,
recoverableErrors: null | Array<CapturedValue<mixed>>,
transitions: Array<Transition> | null,
renderPriorityLevel: EventPriority,
spawnedLane: Lane,
) {
// If there are pending passive effects, schedule a callback to process them.
// Do this as early as possible, so it is queued before anything else that
// might get scheduled in the commit phase. (See #16714.)
// TODO: Delete all other places that schedule the passive effect callback
// They're redundant.
if (
(finishedWork.subtreeFlags & PassiveMask) !== NoFlags ||
(finishedWork.flags & PassiveMask) !== NoFlags
) {
if (!rootDoesHavePassiveEffects) {
scheduleCallback(NormalSchedulerPriority, () => {
flushPassiveEffects();
return null;
});
}
}
if (subtreeHasEffects || rootHasEffect) {
const shouldFireAfterActiveInstanceBlur = commitBeforeMutationEffects(
root,
finishedWork,
);
// The next phase is the mutation phase, where we mutate the host tree.
commitMutationEffects(root, finishedWork, lanes);
commitLayoutEffects(finishedWork, root, lanes);
}
// Always call this before exiting `commitRoot`, to ensure that any
// additional work on this root is scheduled.
ensureRootIsScheduled(root);
// If layout work was scheduled, flush it now.
flushSyncWorkOnAllRoots();
return null;
}
而useLayoutEffect
的回调和销毁函数则在commitLayoutEffects
方法中被高优先级调度
而这个时候react的dom操作都已经操作完了,已经到了浏览器的layout
阶段,但是useLayoutEffect
的微任务调度导致它的任务可以被浏览器保证执行,也就阻塞了浏览器的绘制,直到其任务执行完,浏览器才能执行paint
阶段
最后
以上内容如有纰漏,敬请指教