useLayoutEffect 与 useEffect 类似,区别是 useLayoutEffect 会在重绘之前同步执行。
为什么会闪烁?
可以点击文本 effect 和 layoutEffect 来体验两者在更新时的区别。(增加 delay 同步任务,方便调试)
可以发现两者之间已经区别很小了,但是 useEffect 在更新时仍会出现闪烁。
分析
useEffect
从 performance 面板中可以看到在 click 事件触发后,直到 delay 函数执行完毕,才有了第一次的 Layout 布局计算,此次更新为 click 中的 setState 更新,在后面的 Task 的 Layout 为 useEffect 中的 setState 更新。
useLayoutEffect
而点击 useLayoutEffect 只有一次 Layout 计算,且在同一个 Task 中完成。
为什么会等 useLayoutEffect 执行完毕再计算 Layout ?
源码部分
直接从 commit 阶段开始分析,可以结合上面的 performance 截图查看函数的执行关系与顺序方便理解。
commitRootImpl
在 commitRootImpl 检测到有副作用后直接同步调用 commitLayoutEffects,并设置当前任务的执行优先级为 SyncLane,在 ensureRootIsScheduled 中会根据优先级 updatePriority (getNextLanes => fiber.pendingLanes) 确定任务的调度关系,并将任务放入同步队列。
虽然 useLayoutEffect 在 render 后执行,但函数中的 dom 变更将在 commitMutationEffects 中处理,所以两者将在同一任务队列中在一个宏任务中执行。
react/packages/react-reconciler/src/ReactFiberWorkLoop.js
// 设置 fiber.flags,在注册 effects 时同 function updateEffectImpl(fiberFlags: Flags, hookFlags: HookFlags): void { // .... currentlyRenderingFiber.flags |= fiberFlags; } // layoutEffect 的 fiber.flags function updateLayoutEffect( create: () => (() => void) | void, deps: Array<mixed> | void | null, ): void { return updateEffectImpl(Update, HookLayout, create, deps); } // fiber flags,useEffectLayout 的 fiber.flags 为 Update const LayoutMask = Update | Callback | Ref | Visibility; function commitRootImpl( root: FiberRoot, // ... ) { const subtreeHasEffects = (finishedWork.subtreeFlags & (BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !== NoFlags; const rootHasEffect = finishedWork.flags & LayoutMask; // 与 subtreeHasEffects 一致 // Check if there are any effects in the whole tree if (subtreeHasEffects || rootHasEffect) { /** * 1. 保存当前的任务优先级 * 2. 设置优先级为 DiscreteEventPriority => SyncLane => 1 * 3. 所以此时若 useLayoutEffect 内部含有 setState 的话,他们的 updatePriority 保持一致。 * 4. setState 调用 dispatchSetState 时,scheduleUpdateOnFiber(内部 markRootUpdated同时也设置 pendingLanes 为 updatePriority) => ensureRootIsScheduled => getNextLanes 时 * 根据 pendingLanes(或其他 Lanes) 的优先级,从而决定触发更新时是否同步或异步任务 * (异步任务 performConcurrentWorkOnRoot,其实内部也能输出同步任务,但这个函数是异步调度过来的,所以会有一次渲染过程) */ const prevTransition = ReactSharedInternals.T; ReactSharedInternals.T = null; const previousPriority = getCurrentUpdatePriority(); // 设置同步优先级 setCurrentUpdatePriority(DiscreteEventPriority); // 处理与 DOM 变更相关,如 render 后执行 dom 变更 commitMutationEffects(root, finishedWork, lanes); // 同步调用 useLayoutEffects commitLayoutEffects(finishedWork, root, lanes); // 执行完成后恢复刚刚的优先级 executionContext = prevExecutionContext; // Reset the priority to the previous non-sync value. setCurrentUpdatePriority(previousPriority); ReactSharedInternals.T = prevTransition; } }
在上面执行 commitLayoutEffects 前,设置 setCurrentUpdatePriority 为 DiscreteEventPriority(DiscreteEventPriority = SyncLane),并在 ensureRootIsScheduled 中根据 updatePriority 判断任务调度关系是否为同步。
// updateContainer 通过此函数获取 UpdateLane 后并传递给 scheduleUpdateOnFiber
function requestUpdateLane(fiber) {
var mode = fiber.mode;
if ((mode & ConcurrentMode) === NoMode) {
return SyncLane;
}
// ....
var updateLane = getCurrentUpdatePriority();
if (updateLane !== NoLane) {
return updateLane;
}
}
// 在 scheduleUpdateOnFiber 中设置为 updatePriority
function markRootUpdated(root, updateLane, eventTime) {
root.pendingLanes |= updateLane;
}
function getNextLanes(root, wipLanes, name) {
// 在 markRootUpdated 中设置为 updatePriority
var pendingLanes = root.pendingLanes;
var nextLanes = NoLanes;
var suspendedLanes = root.suspendedLanes;
var pingedLanes = root.pingedLanes;
var nonIdlePendingLanes = pendingLanes & NonIdleLanes;
if (nonIdlePendingLanes !== NoLanes) {
var nonIdleUnblockedLanes = nonIdlePendingLanes & ~suspendedLanes;
if (nonIdleUnblockedLanes !== NoLanes) {
nextLanes = getHighestPriorityLanes(nonIdleUnblockedLanes);
} else {
var nonIdlePingedLanes = nonIdlePendingLanes & pingedLanes;
if (nonIdlePingedLanes !== NoLanes) {
nextLanes = getHighestPriorityLanes(nonIdlePingedLanes);
}
}
} else {
// The only remaining work is Idle.
var unblockedLanes = pendingLanes & ~suspendedLanes;
if (unblockedLanes !== NoLanes) {
nextLanes = getHighestPriorityLanes(unblockedLanes);
} else {
if (pingedLanes !== NoLanes) {
nextLanes = getHighestPriorityLanes(pingedLanes);
}
}
}
// ...
return nextLanes;
}
// 编译后的 18.3 源码
function ensureRootIsScheduled(root, currentTime, name) {
var existingCallbackNode = root.callbackNode; // Check if any lanes are being starved by other work. If so, mark them as
// 获取当前任务的优先级
var nextLanes = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
'ensureRootIsScheduled',
);
if (existingCallbackNode != null) {
// Cancel the existing callback. We'll schedule a new one below.
cancelCallback$1(existingCallbackNode);
}
var newCallbackNode;
// 判断是否需要同步调度
if (newCallbackPriority === SyncLane) {
// 放入同步任务队列
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
} else {
// 异步调度
newCallbackNode = scheduleCallback$1(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root),
);
}
root.callbackPriority = newCallbackPriority;
root.callbackNode = newCallbackNode;
}
结论
简单理解的话,就是 useLayoutEffect 和 dom 变更在一个宏任务(同步任务)中执行。useLayoutEffect 在 render 后 React 刚变更完 dom(commitMutationEffects 如 node.textContent = text)后执行,此时的 dom 变更还未被浏览器绘制,所以在 useLayoutEffect 中进行 dom 变更不会闪烁(浏览器会优化在同一个任务中的 连续变更 dom 操作)。
那为什么在 useEffect 中变更 dom 会闪烁呢?
在 flushPassiveEffects 中执行 useEffect 的 cleanUp 和 create 函数。在 commitRootImpl 中默认异步调度(scheduleCallback)执行 flushPassiveEffects,所以 useEffect 在设置 dom 后的“下个”宏任务才执行,此时的 dom 已经绘制完成,在 useEffect 中再更新 dom 会有闪烁(设备较差时更为明显,由于浏览器的优化性能较好的设备差异较小)。
但是在用户交互事件中,例如点击、输入等,将会在此次任务中同步执行 flushPassiveEffects,会同步执行 create 函数,若内部有更新 dom 操作(如 setState 操作),将会作为异步任务被调度。也是因为如此,在上面的 demo 中,点击后 useEffect 在 render 后立即执行,但 dom 的更新任务将在下次任务调度中执行,所以体感上并没有明显的 dom 闪烁,但是仍会闪烁的原因。
// 创建 同
function updateEffect(create, deps) {
return updateEffectImpl(Passive, Passive$1, create, deps);
}
function commitRootImpl(
root: FiberRoot,
// ...
) {
// 检测是否存在 effect
if (
(finishedWork.subtreeFlags & PassiveMask) !== NoFlags ||
(finishedWork.flags & PassiveMask) !== NoFlags
) {
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
pendingPassiveTransitions = transitions;
// 异步调度
scheduleCallback$1(NormalPriority, function () {
flushPassiveEffects('scheduleCallback$1');
return null;
});
}
}
if (subtreeHasEffects || rootHasEffect) {
// ...
}
// If the passive effects are the result of a discrete render, flush them
// synchronously at the end of the current task so that the result is
// immediately observable. Otherwise, we assume that they are not
// order-dependent and do not need to be observed by external systems, so we
// can wait until after paint.
// TODO: We can optimize this by not scheduling the callback earlier. Since we
// currently schedule the callback in multiple places, will wait until those
// are consolidated.
// 如果 useEffect 的副作用是由离散渲染(discrete render)导致的(例如点击、输入等离散用户交互事件),那么在当前任务结束时同步刷新它们,以便结果能立即被观察到。
// 否则,假设这些副作用不是顺序依赖的,也不需要被外部系统观察,因此可以等到绘制(paint)之后再执行。
if (
includesSyncLane(pendingPassiveEffectsLanes) &&
(disableLegacyMode || root.tag !== LegacyRoot)
) {
flushPassiveEffects();
}
}
function flushPassiveEffects(params) {
if (rootWithPendingPassiveEffects !== null) {
var renderPriority = lanesToEventPriority(pendingPassiveEffectsLanes);
// DefaultLane
var priority = lowerEventPriority(DefaultEventPriority, renderPriority);
var prevTransition = ReactCurrentBatchConfig$3.transition;
var previousPriority = getCurrentUpdatePriority();
try {
ReactCurrentBatchConfig$3.transition = null;
// 设置当前任务优先级
setCurrentUpdatePriority(priority);
// 执行副作用 effect
return flushPassiveEffectsImpl(params);
} finally {
setCurrentUpdatePriority(previousPriority);
ReactCurrentBatchConfig$3.transition = prevTransition; // Once passive effects have run for the tree - giving components a
}
}
return false;
}
setState 是 “同步” “异步”?
一步一步来,先看 setState 对应的的 dispatchSetState 函数,可以看到如果你的 setState 传递的是函数的话,是会被同步执行(同步执行 actions 函数获得最新 state),如果得到的值与上次的值不相同,则将此次 update push 到 concurrentQueues 中,由后续任务调度执行。
这里也能看到老朋友 scheduleUpdateOnFiber,由他调用 ensureRootIsScheduled,再根据当前的 UpdatePriority 来决定是否同步或异步调度任务。
function basicStateReducer(state, action) {
return typeof action === 'function' ? action(state) : action;
}
function mountState(initialState) {
var hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
var queue = {
pending: null,
interleaved: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: initialState,
};
hook.queue = queue;
var dispatch = (queue.dispatch = dispatchSetState.bind(null, currentlyRenderingFiber$1, queue));
return [hook.memoizedState, dispatch];
}
function dispatchSetState(fiber, queue, action) {
// 获取当前优先级
var lane = requestUpdateLane(fiber, 'requestUpdateLane');
var update = {
lane: lane,
action: action,
hasEagerState: false,
eagerState: null,
next: null,
};
// 处理在 render 中的 setState
if (isRenderPhaseUpdate(fiber)) {
enqueueRenderPhaseUpdate(queue, update);
} else {
var alternate = fiber.alternate;
if (fiber.lanes === NoLanes && (alternate === null || alternate.lanes === NoLanes)) {
var lastRenderedReducer = queue.lastRenderedReducer;
if (lastRenderedReducer !== null) {
try {
var currentState = queue.lastRenderedState;
// lastRenderedReducer 为 basicStateReducer,获取这次更新的 state,例如 action 是函数就执行
var eagerState = lastRenderedReducer(currentState, action);
update.hasEagerState = true;
update.eagerState = eagerState;
// 两次 setState 的 state 一致不进行 后续操作
if (objectIs(eagerState, currentState)) {
enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update, lane);
return;
}
} catch (error) {}
}
}
// 将创建的 update 对象放入 concurrentQueues 队列中
var root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
if (root !== null) {
// 执行任务调度
scheduleUpdateOnFiber(root, fiber, lane, eventTime);
}
}
}
所以,同上面的 useEffect 运行逻辑比较相像的地方来了,在用户交互事件中,或一些其他场景下例如:useLayoutEffect 中,React 会将优先级设置为 SyncLane,在 ensureRootIsScheduled 中,使用 scheduleMicrotask(优先 queueMicrotask、Promise.then、setTimeout)进行微任务调度。
如果优先级低的任务,在 ensureRootIsScheduled 中当做异步任务中处理。
所以在 useLayoutEffect 中 setState 时,无论在不在交互事件,也会将优先级设置为 SyncLane,此时的 setState 的变更任务会放入此次循环中的微任务中进行处理。
function ensureRootIsScheduled(root, currentTime, name) {
var existingCallbackNode = root.callbackNode; // Check if any lanes are being starved by other work. If so, mark them as
// 获取当前任务的优先级
var nextLanes = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
'ensureRootIsScheduled',
);
var existingCallbackPriority = root.callbackPriority;
// 相同放入调度场景只会产生一次调度,例如多个 setState 时的处理
if (
existingCallbackPriority === newCallbackPriority &&
!(ReactCurrentActQueue$1.current !== null && existingCallbackNode !== fakeActCallbackNode)
) {
// ...
return;
}
var newCallbackNode;
// 判断是否需要同步调度
if (newCallbackPriority === SyncLane) {
// 放入同步任务队列
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
if (ReactCurrentActQueue$1.current !== null) {
ReactCurrentActQueue$1.current.push(flushSyncCallbacks);
} else {
// 微任务调度
scheduleMicrotask(function () {
if ((executionContext & (RenderContext | CommitContext)) === NoContext) {
flushSyncCallbacks();
}
});
}
} else {
// 异步调度
newCallbackNode = scheduleCallback$1(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root),
);
}
root.callbackPriority = newCallbackPriority;
root.callbackNode = newCallbackNode;
}
哪些事件被设置为 SyncLane ?
function getEventPriority(domEventName) {
switch (domEventName) {
// Used by SimpleEventPlugin:
case 'cancel':
case 'click':
case 'close':
case 'contextmenu':
case 'copy':
case 'cut':
case 'auxclick':
case 'dblclick':
case 'dragend':
case 'dragstart':
case 'drop':
case 'focusin':
case 'focusout':
case 'input':
case 'invalid':
case 'keydown':
case 'keypress':
case 'keyup':
case 'mousedown':
case 'mouseup':
case 'paste':
case 'pause':
case 'play':
case 'pointercancel':
case 'pointerdown':
case 'pointerup':
case 'ratechange':
case 'reset':
case 'resize':
case 'seeked':
case 'submit':
case 'touchcancel':
case 'touchend':
case 'touchstart':
case 'volumechange': // Used by polyfills:
// eslint-disable-next-line no-fallthrough
case 'change':
case 'selectionchange':
case 'textInput':
case 'compositionstart':
case 'compositionend':
case 'compositionupdate': // Only enableCreateEventHandleAPI:
// eslint-disable-next-line no-fallthrough
case 'beforeblur':
case 'afterblur':
case 'beforeinput':
case 'blur':
case 'fullscreenchange':
case 'focus':
case 'hashchange':
case 'popstate':
case 'select':
case 'selectstart':
return DiscreteEventPriority;
default:
return DefaultEventPriority;
}
}
function createEventListenerWrapperWithPriority(targetContainer, domEventName, eventSystemFlags) {
var eventPriority = getEventPriority(domEventName);
var listenerWrapper;
switch (eventPriority) {
case DiscreteEventPriority:
// 设置优先级为 DiscreteEventPriority => setCurrentUpdatePriority(DiscreteEventPriority);
listenerWrapper = dispatchDiscreteEvent;
break;
default:
listenerWrapper = dispatchEvent;
break;
}
return listenerWrapper.bind(null, domEventName, eventSystemFlags, targetContainer);
}
看图说话
左侧为 click 执行的 setState 流程,右侧为 setTimeout 中的 setState 流程。
在事件中,React 会将优先级设置为 SyncLane,并在微任务(Microtasks)中执行。在定时器中(Timer fired)优先级为 DefaultLane,在异步任务(Macrotasks)中执行,可以看到有三个 Task:1. setTimeout 中 setState 2. 处理 setState 产生的变更 3. 渲染
[{"url":"https://static.ksh7.com/post/react-useLayoutEffect-eventLoop/0085UwQ9gy1hrgq0ubtzmj31m40iq15s.webp?imageMogr2/thumbnail/!50p","dataset":{"originPic":"https://static.ksh7.com/post/react-useLayoutEffect-eventLoop/0085UwQ9gy1hrgq0ubtzmj31m40iq15s.webp","thumbnail":""}},{"url":"https://static.ksh7.com/post/react-useLayoutEffect-eventLoop/0085UwQ9gy1hrgq0uc1dmj31xg0ka7kp.webp?imageMogr2/thumbnail/!50p","dataset":{"originPic":"https://static.ksh7.com/post/react-useLayoutEffect-eventLoop/0085UwQ9gy1hrgq0uc1dmj31xg0ka7kp.webp","thumbnail":""}}]
一个 demo 让你更直观地理解 useLayoutEffect 和 useEffect
当然,看两行代码不如写两行代码实在。模拟一下在事件中 useLayoutEffect 和 useEffect 中变更 dom 的区别。
<body>
<div style="display: flex; gap: 8px">
<button class="effect-button">effect</button>
<button class="layout-effect-button">layout effect</button>
<button class="rest-effect">rest</button>
</div>
<div style="position: relative; margin-top: 20px">
<div class="effect-box">1</div>
</div>
</body>
<script>
function effectLogic() {
const box = document.querySelector('.effect-box');
const effectButton = document.querySelector('.effect-button');
const layoutButton = document.querySelector('.layout-effect-button');
const resetButton = document.querySelector('.rest-effect');
var variable = 1;
const delay = () => {
let now = 9e8 || performance.now();
while (now > 10) {
now -= 1;
}
console.log('done');
};
const setState = (isReset, isSync) => {
const setDom = () => {
if (isReset) {
box.style.transform = 'translate3d(0, 0, 0)';
box.textContent = '0';
variable = 0;
} else {
box.style.transform = 'translate3d(100px, 0, 0)';
box.textContent = '2';
variable = 1;
}
};
if (isSync) {
queueMicrotask(() => {
setDom();
});
} else {
setTimeout(() => {
setDom();
});
}
};
const effect = () => {
delay();
setState();
console.log('effect run, current variable', variable); // expect 1, result 0
};
const layoutEffect = () => {
// delay();
// 微任务也会延迟渲染的执行
// queueMicrotask(() => {
// delay();
// })
// 延迟微任务的执行
delay();
setState(false, true);
console.log('effect run, current variable', variable);
};
layoutButton.addEventListener('click', () => {
setState(true, true);
layoutEffect();
});
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = () => {
effect();
};
effectButton.addEventListener('click', () => {
setState(true, true);
// 交互事件中为同步,其他情况异步调度,如使用 MessageChannel
effect();
// port.postMessage(null);
// setTimeout(() => {
// effect();
// })
});
resetButton.addEventListener('click', () => {
box.style.transform = 'translate3d(20px, 0, 0)';
box.textContent = '0';
variable = 0;
});
}
effectLogic();
</script>
1
























