useLayoutEffect

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. 渲染

一个 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