【React】结合源码和 EventLoop 分析 - 为什么 useLayoutEffect 会阻止 DOM 重绘(而 useEffect 闪烁)?为什么其内部 useState 会“同步”执行?
useLayoutEffect
为什么会闪烁?
分析
useEffect
useLayoutEffect
为什么会等 useLayoutEffect
执行完毕再计算 Layout
?
源码部分
commitRootImpl
// 设置 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;
}
}
// 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;
}
结论
那为什么在 useEffect
中变更 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
是 “同步” “异步”?
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);
}
}
}
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);
}
看图说话
[{"url":"https://image.baidu.com/search/down?url=https://gzw.sinaimg.cn/mw2000/0085UwQ9gy1hrgq0ubtzmj31m40iq15s.jpg","alt":""},{"url":"https://image.baidu.com/search/down?url=https://gzw.sinaimg.cn/mw2000/0085UwQ9gy1hrgq0uc1dmj31xg0ka7kp.jpg","alt":""}]
一个 demo 让你更直观地理解 useLayoutEffect
和 useEffect
<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
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 kshao-blog-前端知识记录!
评论