惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
人人都是产品经理
人人都是产品经理
Cisco Talos Blog
Cisco Talos Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
V
V2EX
博客园 - 三生石上(FineUI控件)
Martin Fowler
Martin Fowler
WordPress大学
WordPress大学
D
Docker
S
SegmentFault 最新的问题
博客园 - 聂微东
美团技术团队
Apple Machine Learning Research
Apple Machine Learning Research
月光博客
月光博客
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Last Week in AI
Last Week in AI
M
MIT News - Artificial intelligence
F
Fortinet All Blogs
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
The GitHub Blog
The GitHub Blog
GbyAI
GbyAI
L
LangChain Blog
Vercel News
Vercel News
博客园 - 叶小钗
MongoDB | Blog
MongoDB | Blog
Stack Overflow Blog
Stack Overflow Blog
H
Help Net Security
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
The Cloudflare Blog
Engineering at Meta
Engineering at Meta
T
Threat Research - Cisco Blogs
T
Threatpost
Scott Helme
Scott Helme
T
Tailwind CSS Blog
Latest news
Latest news
Stack Overflow Blog
Stack Overflow Blog
Blog — PlanetScale
Blog — PlanetScale
The Register - Security
The Register - Security
罗磊的独立博客
P
Proofpoint News Feed
腾讯CDC
S
Schneier on Security
雷峰网
雷峰网
A
About on SuperTechFans
T
Tenable Blog
F
Full Disclosure
Cyberwarzone
Cyberwarzone
博客园_首页
有赞技术团队
有赞技术团队
K
Kaspersky official blog

文章列表

win or mac clash 无 TUN 让 Antigravity、Chrome 强制 proxy(解决 Antigravity 无法加载选择 model、自动更新无法登录、跳转) 【大杂烩】在 pnpm 中直接修改 node_modules(.pnpm) 中的依赖项,项目中持久化 - pnpm 中的依赖处理、幽灵依赖、寻址规则等 在 html 中直接使用 Esm、Jsx 脚本快速调试和使用 React@19 和 Vue@3 源码,解决 React19 UMD 构建等问题 一键在本地批量检测并升级更新 package.json 中的模块依赖,ncu(npm-check-updates)在 npm、pnpm 或 workspace 项目中的使用教程 解决 Mac Docker Desktop 中启动出现的问题合集 通过阿里云、腾讯云无服务器搭建自定义的企业域名邮箱,实现在 QQ邮箱 收发等功能(附腾讯 SMTP 和 IMAP) 解决使用代理(clash 等)进行 SSH 连接(如 Github ssh key clone/push)出现 kex_exchange_identification 错误 静态文件资源 cdnjs, jsdelivr 抖音字节国内快速 CDN 镜像推荐【2025】- 仍在使用 bootcdn 和 staticfile CDN 请注意验证资源的完整性(SRI) pnpm monorepo 中管理依赖的最佳实践,与 Catalogs(目录)协议的使用(monorepo 中统一版本管理) Web 安全中的 Secure Contexts(安全上下文)- 解决在本地中使用 clipboard 或 Crypto 等 API 限制或关闭上下文限制 使用 serve 配合 openssl 或 mkcert 创建本地自签名可信任的证书 - 创建本地 TLS\SSL https 协议服务 利用 Github Actions 和 Acme 自动申请、更新和部署至阿里云、腾讯云 CDN Lets Encrypt SSl\TLS ECC RSA 双证书 【CSS】解决在 flex 容器中使用 align-content 或 justify-content 属性 center 居中时的溢出滚动和截断问题 - 理解 safe 关键字 在线工具 - 一键获取下载抖音无水印视频、抖音去水印解析工具、下载抖音无水印高清图集【2025 最新】 【React Router】v6 data router 在非组件(或工具方法)中如何优雅的跳转路由 【React】为什么路由跳转时页面滚动高度不会被重置(保留上个页面高度)?理解 history scrollRestoration 的场景与使用,以及如何使用 React Router 重置和跳转前保留滚动高度 【React】在本地 Html 中快速 debug(调试)React 源码 React Compiler - 解放在函数中编程时的性能焦虑(React Conf 2024)附 Next 在线演示 在 github actions 中获取时间,并转换为中国标准时间(中国时区) 【npm】npm ci - npm clean install,在 CI、CD 中保持构建的一致性和可重复性 eslint 9.x 升级或使用指南,eslint.config.js 配置,包含 react、typescript、prettier 等常用配置升级迁移 使用 Spicetify 自定义 Spotify - 歌词翻译、全屏展示、主题替换
【React】结合源码和 EventLoop 分析 - 为什么 useLayoutEffect 会阻止 DOM 重绘(而 useEffect 闪烁)?为什么其内部 useState 会“同步”执行?
kshao · 2024-07-04 · via

useLayoutEffectuseEffect 类似,区别是 useLayoutEffect 会在重绘之前同步执行。

为什么会闪烁?

可以点击文本 effectlayoutEffect 来体验两者在更新时的区别。(增加 delay 同步任务,方便调试)

可以发现两者之间已经区别很小了,但是 useEffect 在更新时仍会出现闪烁。

分析

useEffect

performance 面板中可以看到在 click 事件触发后,直到 delay 函数执行完毕,才有了第一次的 Layout 布局计算,此次更新为 click 中的 setState 更新,在后面的 TaskLayoutuseEffect 中的 setState 更新。

useLayoutEffect

而点击 useLayoutEffect 只有一次 Layout 计算,且在同一个 Task 中完成。

为什么会等 useLayoutEffect 执行完毕再计算 Layout

源码部分

直接从 commit 阶段开始分析,可以结合上面的 performance 截图查看函数的执行关系与顺序方便理解。

commitRootImpl

commitRootImpl 检测到有副作用后直接同步调用 commitLayoutEffects,并设置当前任务的执行优先级为 SyncLane,在 ensureRootIsScheduled 中会根据优先级 updatePriority (getNextLanes => fiber.pendingLanes) 确定任务的调度关系,并将任务放入同步队列。

虽然 useLayoutEffectrender 后执行,但函数中的 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 前,设置 setCurrentUpdatePriorityDiscreteEventPriorityDiscreteEventPriority = 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;
}

结论

简单理解的话,就是 useLayoutEffectdom 变更在一个宏任务(同步任务)中执行。useLayoutEffectrenderReact 刚变更完 domcommitMutationEffectsnode.textContent = text)后执行,此时的 dom 变更还未被浏览器绘制,所以在 useLayoutEffect 中进行 dom 变更不会闪烁(浏览器会优化在同一个任务中的 连续变更 dom 操作)。

那为什么在 useEffect 中变更 dom 会闪烁呢?

flushPassiveEffects 中执行 useEffectcleanUpcreate 函数。在 commitRootImpl 中默认异步调度(scheduleCallback)执行 flushPassiveEffects,所以 useEffect 在设置 dom 后的“下个”宏任务才执行,此时的 dom 已经绘制完成,在 useEffect 中再更新 dom 会有闪烁(设备较差时更为明显,由于浏览器的优化性能较好的设备差异较小)。

但是在用户交互事件中,例如点击、输入等,将会在此次任务中同步执行 flushPassiveEffects,会同步执行 create 函数,若内部有更新 dom 操作(如 setState 操作),将会作为异步任务被调度。也是因为如此,在上面的 demo 中,点击后 useEffectrender 后立即执行,但 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 pushconcurrentQueues 中,由后续任务调度执行。

这里也能看到老朋友 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(优先 queueMicrotaskPromise.thensetTimeout)进行微任务调度。
如果优先级低的任务,在 ensureRootIsScheduled 中当做异步任务中处理。

所以在 useLayoutEffectsetState 时,无论在不在交互事件,也会将优先级设置为 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. setTimeoutsetState 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 让你更直观地理解 useLayoutEffectuseEffect

当然,看两行代码不如写两行代码实在。模拟一下在事件中 useLayoutEffectuseEffect 中变更 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