异步与事件循环

共 46 题
📑 题目列表 46 题
#
★★★

1. async/await 与微任务调度机制

async/await 是如何与微任务调度机制协作的?await 后面的代码何时执行?

  • async 函数返回 Promise 与状态包装
  • await 挂起函数、恢复时排入微任务
  • 每次 await 都产生微任务跳点的执行时序

async 函数内部执行同步代码直到第一个 await,之后函数挂起,await 表达式求值完成后,函数体的剩余部分被调度为微任务继续执行;async 函数的返回值会被包装为 Promise,内部抛错导致 Promise 拒绝。await 的语义细节:await 的值若不是 Promise 会被 Promise.resolve 包装,恢复执行一定发生在当前宏任务内所有同步代码与既有微任务之后(每次 await 都是一次微任务跳点),因此多个 await 之间存在"同步代码优先"的时序保证。工程上:await 链中每步错误可用 try/catch 统一捕获(async 内部同步错误也转为拒绝);注意不要把 await 用于同步值(多余微任务)或遗忘 await 导致并发副作用。

本题考察 async/await 与事件循环的衔接。核心是"await 恢复走微任务、每次 await 一个跳点"的调度语义,能画出一条代码的微任务执行时间线即证明掌握;补充错误传播(async 内 throw 变 rejection)与工程注意点使答案完整。

#
★★★

2. Promise 状态机与 then/catch/finally 链式行为

Promise 的状态机是怎样的?then/catch/finally 在链式调用中的行为有何区别?

  • pending/fulfilled/rejected 三态与单向转移
  • then 返回新 Promise 与值/拒绝的扁平化
  • catch/finally 的语义与链式位置

Promise 有 pending、fulfilled、rejected 三态,只能从 pending 单向转移且不可逆,状态与结果(value/reason)一经确定永久保留。then(onFulfilled, onRejected) 返回新的 Promise:回调返回非 Promise 值则新 Promise 兑现该值,返回 Promise 则状态跟随(扁平化吸收),回调抛错则新 Promise 拒绝,因此链可以无限串联并统一收尾。catch 等价于 then(undefined, onRejected),只处理前序拒绝;finally 无论兑现或拒绝都执行清理回调,但它的返回值被忽略(除非抛错),原状态与结果会继续向后传递。工程上:链式错误处理用末端 catch 兜底,清理工作放 finally,避免在 then 中重复捕获。

本题考察 Promise 链式语义的核心。答题框架:状态机单向性→then 的扁平化与错误传播→catch/finally 的位置语义,能解释"finally 不改写值但改写拒绝"即展示规范级理解。

#
★★★

3. 事件循环的微任务 checkpoint,Promise 回调、MutationObserver 与 DOM 更新批量化的执行时机

事件循环中的微任务 checkpoint 是什么?Promise 回调、MutationObserver 与 DOM 更新批量化分别在什么时机执行?

  • 微任务队列与 checkpoint 检查点
  • 每个宏任务/脚本结束后清空微任务
  • DOM 更新批量化在微任务 checkpoint 中的合并

微任务队列独立于宏任务队列:每个宏任务执行完毕后、渲染与下一宏任务开始前,事件循环执行"微任务 checkpoint"——把微任务队列中的任务全部取出执行,期间新产生的微任务也继续执行直到队列清空。Promise 回调与 queueMicrotask、MutationObserver 回调都走微任务队列;DOM 更新批量化则表现为:同一宏任务内多次修改 DOM(读写 class、style)会合并到该宏任务后的渲染阶段一次执行,而微任务 checkpoint 发生在渲染之前,因此微任务中读取布局仍可能拿到批量化前的中间状态(除非强制同步布局)。理解该时序对"微任务里改 DOM 是否触发重排、mutation 回调何时触发"的预测至关重要。

本题考察微任务 checkpoint 与渲染时序。关键是建立"宏任务→清空微任务→渲染→下一个宏任务"的循环模型,并把 Promise/MutationObserver 归入微任务、DOM 更新归入渲染阶段;能预测"微任务中改 DOM 在渲染前生效"即掌握要点。

#
★★★

4. setTimeout 嵌套调用的 4ms 钳制与 setTimeout(0) 在任务切片的真实延迟

setTimeout 嵌套调用的 4ms 钳制是什么?setTimeout(0) 在任务切片中的真实延迟是多少?

  • 嵌套超过 5 层后的 4ms 最小延迟
  • 非活动页面/后台标签的延迟放大
  • setTimeout(0) 的实际延迟与任务切片用途

浏览器对 setTimeout 实施延迟钳制:嵌套调用(在定时器回调内部再调用 setTimeout)超过 5 层后,最小延迟被强制为 4ms;页面处于后台或节能模式时延迟进一步放大(常见 1s/1min 级别),这是为降低电量消耗。setTimeout(0) 并不等于立即执行:任务排入宏任务队列,需等当前宏任务、微任务 checkpoint 与渲染完成才执行,实际延迟通常数毫秒以上。工程上 setTimeout(0) 常用于把长任务切块让出主线程(任务切片),配合 requestIdleCallback 或 scheduler 控制节奏;精确计时与动画应使用 performance.now 时间戳与 requestAnimationFrame,不能用 setTimeout 累计。

本题考察定时器的真实时序约束。要点:4ms 钳制针对嵌套、后台放大、setTimeout(0) 只是"尽快"而非即时,并给出任务切片与精确计时的工程对策;能说明钳制目的(节能与防滥用)即完整。

#
★★★

5. Web Locks API(navigator.locks)在异步共享资源排他控制的工程应用

Web Locks API(navigator.locks)如何实现异步共享资源的排他控制?有哪些工程应用?

  • request 的排他/共享锁模式与异步回调
  • 跨标签页、跨 Worker 的锁协调
  • 与普通异步标志位的对比

navigator.locks.request(name, callback) 请求命名锁,同一名字的锁在相同模式下互斥:默认排他锁(exclusive)同一时刻只允许一个持有者,共享锁(mode:"shared")允许多个读取者;callback 在获得锁后异步执行,返回的 Promise 在回调完成时兑现,锁自动释放(回调抛出则锁释放且拒绝)。锁按浏览器上下文(同源标签页、Worker、Service Worker)统一调度,实现跨页面协调,如"只有一个标签页执行后台同步/写 IndexedDB"、"多个标签页串行更新缓存"。工程要点:回调内必须 await 所有异步操作(锁持有期=回调 Promise 生命周期),避免死锁(不要在持有排他锁时再请求同名锁)与超时(可用 AbortSignal 中止等待)。

本题考察跨上下文并发协调的新 API。要点是"命名+模式+回调生命周期"三要素与跨标签页语义,死锁与超时是工程易错点;能对比普通标志位(只防同进程)说明 Web Locks 的跨上下文价值即到位。

#
★★★

6. scheduler.postTask/scheduler.wait 优先级任务调度与事件循环的集成边界

scheduler.postTask 与 scheduler.wait 如何实现优先级任务调度?与事件循环的集成边界是什么?

  • postTask 的 user-blocking/user-visible/background 优先级
  • scheduler.wait 的让步等待与可恢复
  • 与 Promise、requestIdleCallback 的配合边界

scheduler.postTask(fn, {priority}) 把任务按优先级排队:user-blocking(用户可感知阻塞,如渲染关键交互)、user-visible(默认,常规任务)、background(低优先级后台任务),浏览器在每帧调度中按优先级抢占式安排执行,可通过 TaskController 动态调整优先级或中止。scheduler.wait(ms) 返回 Promise,让出主线程指定时间后继续(类似 await 版 sleep),与 postTask 同属 Scheduling API。集成边界:postTask 任务是"任务级"(介于宏任务与帧调度之间),比 setTimeout 更细粒度地被帧调度器感知;与 requestIdleCallback 相比,postTask 有明确优先级与可中止性,而 rIC 只保证空闲时执行;微任务仍优先于一切任务执行。兼容性:Chrome 支持,其他浏览器需特性检测与降级。

本题考察现代调度 API。框架是"优先级三档+任务级调度+可中止"与事件循环各队列的位次关系,再对比 rIC/setTimeout 的差异;能说明微任务仍最优先、postTask 在帧内按优先级插入即展示集成边界的理解。

#
★★★

7. Long Tasks API(PerformanceObserver longtask 条目)在长任务度量的工程价值

Long Tasks API 如何度量长任务?在性能监控上有何工程价值?

  • 长任务定义(>50ms 的阻塞主线程任务)
  • PerformanceObserver 监听 longtask 条目
  • 与 INP/FID 等体验指标的关联

长任务指占用主线程超过 50ms 的连续任务(含其内的脚本执行、样式布局),Long Tasks API 通过 PerformanceObserver 订阅 "longtask" 条目:每条记录任务的开始时间、持续时间与涉及的类型(script/layout 等),可精确量化阻塞主线程的元凶。工程价值:长任务是输入响应延迟(INP、FID)的直接来源,监控长任务可定位卡顿页面与具体时段;条目包含 attribution 信息(引发长任务的顶层脚本),结合 performance.getEntries 与自定义计时可还原任务来源。实践中配合 PerformanceObserver 上报长任务时段、构建长任务水合/交互分析面板,并触发优化(拆任务、交还主线程、懒加载)。

本题考察性能监控基础设施。要点:50ms 阈值定义、longtask 条目字段、与 INP/FID 的因果关联,以及 attribution 的定位能力;能说明"长任务累积是核心 Web 指标恶化的根因"即体现分析深度。

#
★★★

8. 事件循环中 I/O 任务、渲染任务与微任务的优先级分层(浏览器 vs Node.js)

浏览器与 Node.js 的事件循环在 I/O、渲染与微任务的优先级分层上有何差异?

  • 浏览器:宏任务→微任务→渲染→下一宏任务的帧模型
  • Node.js:timers/poll/check 等阶段轮询模型
  • process.nextTick 与微任务的特殊优先级

浏览器以帧为周期:每轮依次执行宏任务、微任务 checkpoint、渲染(样式/布局/绘制),I/O 回调也是宏任务之一,优先级整体低于微任务;渲染受帧率与页面可见性约束。Node.js 采用阶段循环:timers(setTimeout/setInterval)→ pending callbacks → poll(I/O 回调,可阻塞等待)→ check(setImmediate)→ close callbacks,每阶段切换时清空微任务队列;process.nextTick 队列在每阶段结束后、微任务之前优先执行(实际设计为 nextTick 优先于 Promise 微任务)。差异要点:浏览器无 nextTick;Node 的 I/O 有独立阶段与阻塞语义;浏览器把渲染并入循环而 Node 无渲染阶段。工程上跨环境代码避免依赖阶段细节,用 queueMicrotask/Promise 保证一致的微任务语义。

本题考察双环境事件循环的结构对比。建议按"浏览器帧模型 vs Node 阶段模型"对照作答,突出 nextTick 优先级与渲染缺失两个关键差异;能画出两侧循环的简图并指出对应关系即证明系统性理解。

#
★★★

9. Promise.allSettled 的错误隔离与错误聚合(AggregateError)

Promise.allSettled 如何实现错误隔离?错误聚合(AggregateError)在哪些 API 中体现?

  • allSettled 等待全部完成、返回状态数组
  • Promise.any 的 AggregateError 聚合
  • 与 all/race 的错误语义对比

Promise.allSettled(iterable) 等待所有 Promise 落定(无论兑现或拒绝),返回按输入顺序排列的结果数组,每项为 {status:"fulfilled", value} 或 {status:"rejected", reason},任何一项失败都不会让整体失败——这是错误隔离的核心:一批互不依赖的请求可并行发起并逐一归因。错误聚合则相反:Promise.any 等待第一个兑现,全部拒绝时才拒绝,此时拒绝原因是 AggregateError,其 errors 属性包含全部拒绝原因;Promise.all 的拒绝则只取最先失败者。工程上:批量请求各自上报用 allSettled;"至少一个成功"的冗余请求用 any + AggregateError 展开归因;错误处理统一用 reason 类型或 errors 数组分支。

本题考察并发 API 的错误语义对比。核心区分"隔离(allSettled 返回状态数组)"与"聚合(any 拒绝时 AggregateError)",再对照 all 的快速失败;能给出三者选型矩阵即展示工程判断力。

#
★★★

10. 迭代器与生成器协议(Iterator Helpers,map/filter/take/drop)

迭代器与生成器协议是什么?Iterator Helpers(map/filter/take/drop)如何简化迭代操作?

  • 可迭代协议与迭代器协议(next/return/throw)
  • 生成器作为迭代器工厂
  • Iterator Helpers 的惰性链式操作

可迭代协议要求对象有 [Symbol.iterator] 方法返回迭代器;迭代器协议要求对象有 next() 方法返回 {value, done},可选 return/throw 用于提前终止与清理,for...of、展开、解构均依赖这套协议。生成器函数(function*)是迭代器的便捷工厂:yield 暂停/恢复,return 触发迭代器 return 清理。Iterator Helpers(ES2025)为迭代器对象提供 map、filter、take、drop、reduce 等惰性方法:链式调用不立即求值,遍历时逐个变换,避免中间数组分配,适合大流或无限序列(如 take(5) 截取无限生成器);迭代器方法返回新迭代器,可继续链式组合。工程价值:处理流式数据、分页游标、无限序列时以 O(1) 内存完成变换链。

本题考察迭代协议的完整体系。先讲清可迭代/迭代器双协议与生成器的桥梁作用,再落到 Iterator Helpers 的惰性语义与无限序列场景;能说出 done 属性与提前终止(return)的协作即展示细节掌握。

#
★★★

11. import.meta.resolve/import.meta.url 在 ESM 资源解析的工程取舍

import.meta.url 与 import.meta.resolve 在 ESM 资源解析中分别解决什么问题?工程上如何取舍?

  • import.meta.url 提供当前模块 URL
  • import.meta.resolve 解析相对/裸说明符的绝对 URL
  • 动态资源加载、Worker、CSS 的路径基准

import.meta.url 是当前模块的完整 URL(含文件名),作为相对路径的解析基准,解决"当前模块在哪个 URL"的问题:如 new URL("./worker.js", import.meta.url) 生成与模块同目录的资源 URL,比基于 location 拼接更可靠(hash/query 干扰少、不依赖全局)。import.meta.resolve(specifier) 按模块解析规则把说明符(相对路径或裸模块名)解析为绝对 URL,可拿到 Import Map/构建工具映射后的真实地址,用于动态 import、运行时加载、SSR 中的模块定位;其同步返回 URL 字符串(也有 async 变体)。取舍:资源相对当前模块用 import.meta.url + new URL;需要按模块名解析(含别名/映射)用 import.meta.resolve;注意浏览器与 Node 的 resolve 支持差异及 Import Map 的参与。

本题考察 ESM 的 URL 基础设施。要点是"当前模块 URL(url)vs 说明符解析(resolve)"的职责分工与相对路径安全基准的价值;能结合 Worker/动态 import 场景说明使用方式即完整。

#
★★★

12. async generator 的提前退出(return/throw)与 finally 清理时机在异步迭代的工程取舍

async generator 的提前退出(return/throw)如何触发 finally 清理?在异步迭代中有何工程取舍?

  • for await...of 的 break/return 触发迭代器 return
  • async generator 中 finally 的执行时机
  • 资源释放(取消请求、关闭流)的落地

for await...of 循环中 break、return 或外部 throw 时,会调用迭代器的 return()(若存在),async generator 收到 return 后执行其 finally 块完成清理,再结束迭代;generator.throw() 则把错误注入当前 yield 点,使生成器内部抛错并可被 try/catch/finally 捕获,finally 同样先执行。因此 async generator 是"可取消异步流程"的天然载体:循环体内 yield 挂起、外层提前退出时 finally 中做取消(AbortController.abort)、关闭流、释放资源。工程取舍:用 async generator 表达分页拉取、流式读取时,务必在 finally 中清理(关闭 reader、abort fetch),否则提前退出会遗留挂起请求;注意 return() 是异步方法,清理代码应 await 其完成。

本题考察异步迭代的资源生命周期。核心是"提前退出→return()→finally 清理"的链路及其在取消场景的应用;能指出 return() 异步与清理需 await 的细节即体现工程严谨性。

#
★★★

13. JavaScript 装饰器(TC39 Stage 3,access/context 二元形态与 decorator metadata)

TC39 Stage 3 的 JavaScript 装饰器提案采用什么形态?access/context 二元结构与 decorator metadata 是什么?

  • 装饰器接收 value 与 context 二元参数
  • context 的 kind/name/static/private 等字段
  • Symbol.metadata 与元数据收集

Stage 3 装饰器提案把装饰器定义为接收 (value, context) 二元参数的函数:value 是被装饰的目标(类、方法、访问器、字段),context 是描述目标的对象,包含 kind("class"/"method"/"getter"/"setter"/"field"/"accessor")、name、static、private、addInitializer 等;装饰器返回新值替换目标(方法返回新函数、类返回新类),字段类装饰器通过 addInitializer 注入初始化逻辑。与 TypeScript legacy(experimentalDecorators)按 (target, key, descriptor) 签名且参数顺序不同的旧形态有本质差异。decorator metadata 是配套提案:每个类拥有 Symbol.metadata 元数据对象,装饰器可通过 context.metadata 写入、类静态侧读取,用于依赖注入、序列化等反射场景。

本题考察新装饰器提案的形态设计。核心是"(value, context) 二元参数 + 按 kind 分派"与 legacy 的对比,再补充 metadata 机制;能说明为何新形态更利于类型安全与静态分析即展示对提案设计意图的理解。

#
★★★

14. Top-level await 在 ESM 模块的依赖阻塞与加载顺序影响

Top-level await 如何影响 ESM 模块的依赖阻塞与加载顺序?

  • 模块求值的异步化与依赖图阻塞
  • 兄弟模块并行加载、父模块等待
  • 与动态 import、SSR/打包的交互

Top-level await 允许 ESM 顶层 await,模块实例化与求值阶段分离:依赖模块先完成实例化与求值,含 top-level await 的模块在求值时暂停,其导入方(父模块)与同依赖图中"等待该模块"的兄弟模块的求值会被阻塞,直到 await 完成;互不依赖的模块仍可并行求值。影响:模块加载不再是纯同步链,首屏与路由加载时延受模块内 await 影响;循环依赖 + top-level await 可能形成死锁(A await B 而 B await A);打包器需把顶层 await 提升为异步 chunk 边界,Tree Shaking 与静态分析仍可用但副作用顺序需谨慎。工程上:仅在真正需要(如初始化配置、幂等引导)时使用,避免在热路径模块中引入长 await。

本题考察 ESM 求值模型的新语义。要点是"依赖阻塞范围(父与等待方)"与"并行边界",再谈死锁风险与打包影响;能说明为什么顶层 await 会让该模块的依赖方同步等它即抓住核心。

#
★★★

15. queueMicrotask 与 Promise.resolve().then 的等价性与工程取舍

queueMicrotask 与 Promise.resolve().then 是否等价?工程上如何取舍?

  • 二者都排入微任务队列
  • queueMicrotask 无 Promise 包装开销与 then 语义
  • 回调抛错的差异(未捕获拒绝 vs 未捕获异常)

二者都把回调排入微任务队列,执行时机一致,但存在细节差异:queueMicrotask(fn) 直接调度函数,不创建 Promise;Promise.resolve().then(fn) 会创建两个 Promise 对象(resolve 包装与 then 返回值),内存与分配开销略大,且 fn 抛错会变成未处理的 rejection(触发 unhandledrejection),而 queueMicrotask 回调抛错是普通未捕获异常。语义上 queueMicrotask 更纯粹,适合"尽快执行但必须让出当前同步栈"的场景(如 MutationObserver 兼容、微任务队列手动调度);Promise.then 适合已持有 Promise、需要链式衔接的场景。工程取舍:统一调度语义用 queueMicrotask;与 Promise 链集成用 then;框架内部微任务化常用 queueMicrotask 减少对象分配。

本题考察微任务调度的两种写法。核心对比"对象开销与错误形态",能说出 queueMicrotask 回调抛错不走 unhandledrejection 而 Promise 链走,即展示规范级理解;工程结论落到"纯调度用 queueMicrotask、链式用 then"。

#
★★★

16. 微任务饥饿(Microtask Starvation)与渲染阻塞的缓解策略

什么是微任务饥饿?它如何导致渲染阻塞?有哪些缓解策略?

  • 微任务队列必须清空、新微任务持续追加
  • 无限递归微任务使渲染与宏任务无法执行
  • 缓解:宏任务切片、调度 API、限制递归深度

微任务 checkpoint 要求把队列执行到清空,期间新产生的微任务会继续追加并立即执行,若代码以微任务形式无限递归(如 then 中再次 resolve、queueMicrotask 中再排 queueMicrotask),微任务队列永远不会空,渲染、输入事件与宏任务全部被饿死,页面完全卡死。这与 setTimeout 递归不同:宏任务每轮执行后主线程有机会处理其他工作。缓解策略:递归异步任务改用宏任务切分(setTimeout/scheduler.yield 或 postTask)让出主线程;为递归加深度上限并在超限后转宏任务;用 rAF 把每步工作对齐到帧边界;避免在微任务中执行重计算,把大块工作放到 requestIdleCallback。检测:DevTools Performance 中主线程满占且无渲染帧即为饥饿信号。

本题考察微任务队列的"必须清空"语义及其风险。核心是"微任务无限追加→队列不清空→渲染饿死"的因果链,缓解的关键是"改用宏任务/帧边界让出主线程";能解释为何宏任务递归不会饿死渲染即证明理解事件循环本质。

#
★★★

17. Promise.race 在超时控制与最快响应场景的应用边界

Promise.race 在超时控制与"最快响应"场景中的应用边界是什么?

  • race 取最先落定的 Promise(兑现或拒绝)
  • 超时控制中未胜出者仍在运行(副作用与泄漏)
  • 与 AbortSignal.timeout、any 的对比

Promise.race(iterable) 返回的 Promise 随第一个落定的输入 Promise 同步落定(先兑现则兑现、先拒绝则拒绝),用于"取最快":多源响应择优(竞态请求)、超时控制(fetch 与 setTimeout reject 赛跑)。关键边界:race 只决定结果,未胜出的 Promise 仍在后台运行——超时后原 fetch 不会自动取消,其回调与资源占用持续存在,可能造成后续 setState 与内存浪费;race 在输入全部 reject 时会拒绝(与 any 不同,any 只在全部拒绝且聚合)。工程对策:超时场景用 AbortController 显式取消(fetch signal),或 AbortSignal.timeout 自动中止;对"任意成功即可"的需求用 Promise.any;race 结果落定后对落选者做标记忽略(如令牌校验)防止状态竞态。

本题考察 race 的语义与边界。核心是"取最先落定但不取消落选者",超时控制必须配合真实取消机制;能对比 race/any 的拒绝语义与给出竞态防护(令牌)即展示工程完整性。

#
★★★

18. Promise.all/allSettled/race/any 的并发控制差异

Promise.all、allSettled、race、any 在并发控制上有何差异?如何选型?

  • 四者的落定条件与快速失败语义
  • 空数组与拒绝行为
  • 按业务需求(全成功/隔离/最快/任一成功)选型

四个组合器对一批 Promise 并发执行(本身不限制并发数,只编排结果):all 等待全部兑现,任一拒绝则整体拒绝并携带首个拒绝原因(快速失败);allSettled 等待全部落定,返回状态数组(永不整体失败);race 随最先落定者落定(兑现或拒绝皆可);any 等待第一个兑现,全部拒绝时以 AggregateError 聚合。边界:all 与 race 对空数组分别兑现空数组/永久 pending(race 空数组永远 pending 是常见陷阱);any 空数组拒绝 AggregateError。选型:全部必须成功(如多依赖初始化)用 all;批量独立任务逐项归因用 allSettled;最快响应(竞态/超时)用 race;冗余可用性(任一成功即可)用 any;并发数量限制需额外实现(如 p-limit 的信号量池)。

本题考察组合器的完整语义矩阵。建议用"落定条件×失败行为×空输入"三维对比作答,再给选型决策表;能指出 race([]) 永久 pending 与 any([]) 拒绝的边界即证明细致。

#
★★★

19. 宏任务与微任务的执行顺序(Promise、setTimeout、queueMicrotask、requestAnimationFrame)

Promise、setTimeout、queueMicrotask 与 requestAnimationFrame 的执行顺序如何排列?

  • 同步代码→微任务→渲染→宏任务的框架
  • rAF 回调在渲染前、宏任务前执行
  • 多宏任务与多微任务的排队规则

单轮执行顺序为:当前宏任务内同步代码 → 微任务 checkpoint(Promise 回调、queueMicrotask、MutationObserver,直到清空)→ 渲染(样式/布局/绘制,其间执行 requestAnimationFrame 回调)→ 下一个宏任务(setTimeout 等)。rAF 回调在渲染阶段之前执行,因此早于下一轮 setTimeout;但若 rAF 回调中又排入微任务,微任务会在该轮渲染前再次被清空。排队规则:多个 setTimeout 按到期时间排序;微任务按入队顺序;Promise 回调与 queueMicrotask 同队列先进先出。工程预测:console 打印顺序题的核心是先清微任务再看渲染与宏任务,且每次 await 增加微任务跳点;注意浏览器可能在多轮宏任务后合并渲染(非每宏任务都渲染)。

本题考察时序推演能力。给出"宏任务内同步→微任务清空→rAF/渲染→下一宏任务"的骨架即可稳定预测顺序,rAF 与渲染的关系是关键易错点;能解释"微任务中排微任务会先执行完"即展示规范掌握。

#
★★★

20. ESM 静态依赖图与 Tree Shaking 的工程影响

ESM 的静态依赖图如何支撑 Tree Shaking?工程上有哪些注意点?

  • 静态 import/export 的图结构与可达性分析
  • 未使用导出被裁剪的条件(无副作用)
  • sideEffects 标记与死代码消除边界

ESM 的 import/export 在语法层静态可分析,打包器(Rollup/Webpack/esbuild)构建出模块依赖图并做可达性分析:从入口出发,未被引用的导出、未被 import 的模块(无副作用)可被安全裁剪,实现 Tree Shaking;动态 import() 形成独立边界,不影响静态裁剪。工程影响:库作者应提供 ESM 产物并在 package.json 标记 sideEffects:false(无副作用的模块声明)以允许整模块裁剪;代码风格上避免模块顶层副作用(如全局补丁、运行时自执行)、避免用对象整体导入(import * as)代替按名导入、把纯函数与常量独立成小模块;副作用边界(CSS、polyfill 类模块)需显式保留。Tree Shaking 的对象是"未使用代码",质量取决于依赖图纯度。

本题考察模块静态性带来的优化能力。核心链条是"静态语法→依赖图→可达性→裁剪",工程重点在 sideEffects 声明与副作用隔离;能解释 sideEffects:false 的语义与误用风险即展示深层理解。

#
★★

21. AbortSignal 的复合取消(AbortSignal.any)与超时清理的协作

AbortSignal.any 如何实现复合取消?与超时清理如何协作?

  • AbortSignal.any 组合多个信号
  • AbortSignal.timeout 自动超时中止
  • 事件监听清理与 abort 事件协作

AbortSignal.any([s1, s2, ...]) 返回一个组合信号:任一输入信号中止时组合信号同步中止,实现"多个取消来源取或"——用户点击取消、父任务中止、超时三者任一触发即整体取消。AbortSignal.timeout(ms) 返回在指定毫秒后自动中止的信号,可与手动 controller 信号通过 any 组合:fetch(url, {signal: AbortSignal.any([controller.signal, AbortSignal.timeout(8000)])}) 同时获得"手动可取消 + 自动超时"。协作要点:中止由 signal.abort 事件驱动,监听者(fetch、流、定时器)收到后执行清理;组合信号被中止时自身触发 abort 事件,需在回调中移除监听避免重复执行;AbortSignal.any 内部通过监听子信号实现,勿在短期高频场景滥用造成监听器堆积。

本题考察取消机制的组合能力。核心是"any 取或组合 + timeout 自动中止 + abort 事件驱动清理"的协作模型,能给出同时支持手动取消与超时的标准写法即证明掌握。

#
★★

22. AbortController 与 AbortSignal.timeout、AbortSignal.any 的取消传播

AbortController 的取消如何传播?AbortSignal.timeout 与 AbortSignal.any 在其中扮演什么角色?

  • controller.abort() 触发 signal 的 abort 事件
  • 信号沿调用链向下传递
  • timeout/any 的自动与组合取消

AbortController 管理一个 AbortSignal:调用 controller.abort() 使信号置为已中止并触发所有 abort 监听;fetch、流的 reader、定时器等支持 signal 的 API 收到中止后执行自身取消与清理,实现从"用户/上层逻辑"到"底层操作"的取消传播。传播模式:上层创建 controller,把 signal 传给所有下层异步操作(多个 fetch、流、定时器),一次 abort 全链路取消;AbortSignal.timeout(ms) 自动中止的 signal 可在过期时触发同样的传播;AbortSignal.any 把多个信号(手动+超时+父级)组合成一个传播入口。注意点:已中止信号传入 fetch 会立即以 AbortError 拒绝;signal 可跨 Worker/iframe 传递实现外部取消。

本题考察取消传播的机制设计。框架是"controller 发信号→abort 事件→API 自行清理"的单向传播链,timeout/any 是信号的两个特殊来源;能说明 signal 复用与跨上下文传播的边界即展示工程深度。

#
★★

23. 动态 import()、Import Attributes(with { type: ... })的代码分割

动态 import() 如何实现代码分割?Import Attributes(with { type: ... })解决什么问题?

  • import() 返回 Promise、按需加载 chunk
  • 路由懒加载与分包策略
  • Import Attributes 声明模块类型(json/css)

动态 import() 以表达式形式加载模块,返回 Promise,打包器据此把动态依赖拆分为独立 chunk,运行时按需拉取——路由懒加载(组件级 import())、按需第三方库、条件加载是主要应用,配合魔法注释(webpackChunkName)控制分包命名,preload/prefetch 提示提前拉取。Import Attributes(with { type: "json" } 等)为导入声明模块类型:import data from "./cfg.json" with { type: "json" } 显式标记 JSON 模块,浏览器据此选择正确的解析方式(防 MIME 混淆),CSS/HTML 等类型也有对应提案;未声明类型或类型不匹配时导入失败。工程注意:动态 import 的错误处理(网络失败需兜底)、与 SSR 的同构路径解析、Import Attributes 的浏览器支持矩阵与转译(TypeScript 需配置)。

本题考察动态加载与模块类型声明。前半讲 import() 的分包与懒加载实践,后半讲 Import Attributes 的类型安全动机;能说明"动态 import 使打包器产生独立 chunk"与"属性声明约束解析方式"两个层次即完整。

#
★★

24. unhandledrejection 事件的触发时机与职责边界

unhandledrejection 事件在什么时机触发?其职责边界是什么?

  • 微任务轮询结束时仍未处理的拒绝
  • 与 rejectionhandled 的配对
  • 全局兜底上报与业务错误处理的边界

一个 Promise 被拒绝后,若到当前微任务轮询(checkpoint)结束时仍没有挂接拒绝处理(then 的第二个回调或 catch),浏览器触发 window 的 unhandledrejection 事件,事件对象含 reason 与 promise;若之后才补挂处理(如延迟 catch),触发 rejectionhandled 配对事件。触发时机因此是"微任务轮询结束"而非拒绝瞬间,同轮微任务中补上 catch 就不会触发。职责边界:unhandledrejection 是全局兜底(监控上报、统一提示、避免静默丢失),不是业务错误处理的替代——业务层应在 async 链内 try/catch 或 catch 归因,事件里可 preventDefault 抑制默认控制台警告。工程上利用该事件做前端错误监控的 rejection 通道,并对 reason 做类型归一化(Error vs 任意值)。

本题考察拒绝处理的全局机制。核心是"微任务轮询结束时仍未处理才触发"的时机细节与"兜底而非替代"的职责边界;能说出 rejectionhandled 配对事件即展示规范级细节。

#
★★

25. Promise.withResolvers、Promise.try 的语义简化

Promise.withResolvers 与 Promise.try 分别简化了哪些语义?如何使用?

  • withResolvers 一次性取出 resolve/reject 与 promise
  • Promise.try 统一同步/异步函数的错误捕获
  • 与手动 executor 模式对比

Promise.withResolvers() 返回 {promise, resolve, reject} 三个对象,等价于在 executor 里把 resolve/reject 暴露给外部,省去手动闭包,适合"外部事件/回调决定 Promise 落定"的场景(事件监听器、桥接 callback API),且避免了 executor 内 throw 被自动捕获带来的语义混淆。Promise.try(fn) 调用函数并统一包装:fn 同步抛错或返回拒绝 Promise 时,结果 Promise 都一致拒绝,而返回值被吸收,从而让"可能同步可能异步"的函数获得统一的异步错误通道,配合 catch 链即可一处处理两种错误形态(消除了"同步抛错必须 try/catch 包住、异步拒绝必须 catch 链"的分裂)。二者都进入 ES2025 标准,polyfill 实现简单。

本题考察两个语义简化工具。withResolvers 解决"取 resolver"的样板代码,Promise.try 解决"同步异常与异步拒绝统一化";能说出各自消除的痛点场景即证明理解设计动机。

#
★★

26. BroadcastChannel 与 MessageChannel 在跨上下文异步消息传递的差异

BroadcastChannel 与 MessageChannel 在跨上下文消息传递上有何差异?各自适用什么场景?

  • BroadcastChannel 一对多广播、同源跨标签页
  • MessageChannel 一对一管道、port 传递
  • 与 postMessage 的结构化克隆与 Transferable

BroadcastChannel(name) 创建同源内广播通道:同源所有标签页/Worker 中同名通道互相收发消息,是一对多"发布-订阅"模型,无需持有对方引用,适合多标签页同步(存储变更通知、登录状态广播、跨页通信总线)。MessageChannel 创建一对一的双向管道(port1/port2),消息只能由持有对端 port 的上下文收到,port 可经 postMessage 的 Transferable 转移给 Worker/iframe,构建显式消息拓扑(如主线程与 Worker 建立专用通道、流式分块传输)。差异要点:广播 vs 定向、自动同源约束 vs 显式接线、消息都走结构化克隆且 port 可 transfer。工程上:页面间松散同步用 BroadcastChannel;需要可靠定向、大数据分块或管道抽象时用 MessageChannel。

本题考察两种通道的拓扑差异。核心对比"一对多广播 vs 一对一定向"与"同源自动约束 vs 显式接线",能说明 port 的 transfer 能力即展示细节;场景选择是答题落点。

#
★★

27. 异步瀑布流(waterfall)vs 并发,串行 await 与 Promise.all 在依赖请求与并行请求的取舍

异步瀑布流(串行 await)与 Promise.all 并发的取舍是什么?何时必须串行?

  • 串行 await 的时序依赖与总耗时累加
  • Promise.all 并行与最快完成
  • 依赖关系(后一请求需要前一结果)与并发上限

串行 await(瀑布流)逐个执行:后续请求依赖前序结果时无法并行,总耗时为各请求之和;Promise.all 同时发起互不依赖的请求,总耗时约等于最慢者,同时发起还能共享连接与调度。取舍核心是依赖关系:请求 B 需要 A 的响应数据时只能串行;无依赖时并行并注意并发上限(一次性发起过多请求会耗尽连接/触发服务端限流,需用并发池如 p-limit 控制)。工程模式:依赖链用串行 + 缓存中间结果;批量独立请求用 allSettled 并行并逐项归因;"先并行一批,再基于结果串行下一步"的分层混合最常见;对瀑布流要警惕串行放大延迟(每次往返叠加),必要时用请求合并/预取压缩链路。

本题考察异步编排的取舍模型。核心是"依赖决定串行、独立决定并行"以及并发上限治理,能给出混合模式与失败语义选择(allSettled)即展示工程成熟度。

#
★★

28. import.meta.resolve() 在模块路径解析与 SSR 中的应用

import.meta.resolve() 在模块路径解析与 SSR 场景中有哪些应用?

  • 说明符解析为绝对 URL 的同步 API
  • 运行时定位模块真实路径
  • SSR 中解析源码/产物路径的实践

import.meta.resolve(specifier) 按当前模块的解析规则(相对路径、裸说明符、Import Maps 映射)把模块说明符解析为绝对 URL 字符串,返回同步;异步变体 await import.meta.resolve() 可处理动态场景。应用:运行时按模块名获取真实地址(如加载与模块同源部署的静态资源、定位 worker 文件、动态 import 前校验地址);SSR/Node 环境中用于解析框架模块在产物(dist)与源码之间的真实路径,配合 new URL(..., import.meta.url) 计算静态资源根目录,替代脆弱的 __dirname 拼接(ESM 无 __dirname)。注意:浏览器端 resolve 需支持 Import Maps,Node 的 resolve 遵循 node 解析算法;结果 URL 可能带 query/hash,工程上需自行规范化。

本题考察模块解析 API 的工程应用。核心是"说明符→绝对 URL"的标准化解析及其在运行时定位资源、SSR 路径治理中的价值;能说明 ESM 无 __dirname 的痛点与 import.meta.url 的配合即展示实战认知。

#
★★

29. scheduler.yield() 协作式让步在长任务切片的浏览器支持与边界

scheduler.yield() 如何实现协作式让步?在长任务切片中的浏览器支持与边界是什么?

  • yield 返回 Promise、让出主线程至下一可调度点
  • 与 setTimeout(0) 任务切片的对比
  • 兼容性与降级策略

scheduler.yield() 是 Scheduling API 的让步原语:调用后当前任务挂起并让出主线程,返回的 Promise 在浏览器调度器认为合适时(通常下一帧内、按优先级插入)兑现并恢复执行,实现"执行一段→让出→恢复"的协作式长任务切片,让输入事件与渲染有机会插入,避免长任务超过 50ms 损害 INP。与 setTimeout(0) 相比:yield 保序(同优先级任务按 FIFO)、延迟更可预测、不触发 4ms 钳制,且可在 TaskController 上下文内被取消。边界:目前仅 Chromium 支持,需特性检测("scheduler" in globalThis),降级用 setTimeout(0) 或 MessageChannel 切片;让步粒度由调度器决定,不能假设立即恢复;循环内每次迭代都 yield 会产生任务切换开销,需平衡粒度。

本题考察协作式调度的现代原语。要点是"让出至调度器择机恢复"与任务切片的用途,对比 setTimeout 的优势与兼容性降级;能说明粒度平衡(切片太细开销大、太粗阻塞久)即体现工程权衡。

#
★★

30. JavaScript 单线程事件循环与微任务队列(microtask)

JavaScript 单线程事件循环与微任务队列是如何配合工作的?

  • 单线程模型与调用栈
  • 宏任务队列与微任务队列的双队列结构
  • 异步 API 如何回归主线程

JS 是单线程语言:一个主线程顺序执行调用栈中的代码,同步阻塞时页面不可交互,因此所有耗时工作必须异步化。事件循环是单线程的调度中枢:同步代码执行完栈为空后,循环从宏任务队列取任务执行,期间产生的微任务在每轮 checkpoint 清空,再进入渲染与下一宏任务;定时器、网络、事件等由浏览器其他线程完成,完成时把回调放入对应队列回归主线程。微任务队列承载 Promise 回调、queueMicrotask 与 MutationObserver,其"必须清空"语义使 Promise 链在同一轮内无缝衔接,也让微任务在渲染前完成状态更新。理解双队列结构是预测异步时序(Promise vs setTimeout 顺序)与诊断卡顿(长任务、微任务饥饿)的基础。

本题考察事件循环的基础模型。框架是"单线程 + 双队列 + 渲染阶段"的循环模型,微任务清空语义与异步回调的入队方式是重点;能把队列模型应用于具体代码顺序预测即证明掌握。

#
★★

31. async/await 在错误传播与栈追踪相较于 Promise 链的工程取舍

async/await 在错误传播与栈追踪上相较于 Promise 链有何取舍?

  • await 展平链式回调、try/catch 统一捕获
  • 栈追踪的上下文保留与丢失
  • 异步栈与 source map 配合

async/await 把 Promise 链的嵌套回调展平为顺序代码:错误传播上,await 让 try/catch 捕获同步抛错与异步拒绝,语义统一且无需每步挂 catch(漏捕获仍走 unhandledrejection);代码可读性与可维护性显著优于长 then 链。栈追踪上:现代 V8 提供异步栈追踪(Async Stack Traces),await 链中抛错通常能保留调用方上下文,比纯 Promise 链的历史"堆栈断裂"(只显示后续 then 帧)更完整;但深度 await 链在低版本或复杂调度下仍有上下文丢失、栈深度叠加(每 await 一帧)的问题,且 Source Map 还原线上压缩代码栈时需正确处理异步帧。工程取舍:业务编排用 async/await + try/catch;需要流式/并发组合(竞态、聚合)时用 Promise API;错误上报时把异步栈与 cause 链一并采集。

本题考察两种异步写法的错误语义对比。核心是"可读性/捕获统一"与"栈追踪质量"两个维度,异步栈追踪的机制与局限是关键细节;能给出混合使用建议(await 编排 + Promise 组合)即展示工程判断。

#
★★

32. ES Module 与 CommonJS 在循环依赖、默认导出与 tree-shaking 的差异

ES Module 与 CommonJS 在循环依赖、默认导出与 Tree Shaking 上有何差异?

  • CJS 的运行时复制值 vs ESM 的实时绑定
  • ESM 提升与循环依赖的存活语义
  • ESM 静态分析对 Tree Shaking 的支撑

加载机制:CommonJS 在 require 时同步执行模块并复制导出值(后续修改不反映),ESM 先实例化依赖图再求值,导出是实时绑定(live binding),导入方始终读到最新值。循环依赖:CJS 中 A require B 时 B 又 require A,B 拿到的是 A 未完成求值的部分导出(可能 undefined),需延迟访问规避;ESM 由于声明提升与绑定语义,循环依赖通常可正常工作(只要不在模块顶层立即使用未初始化值),但顶层 TDZ 仍可能抛错。默认导出:CJS 的 module.exports 整体是默认值,interop 转换(__esModule 标记)在混合使用时产生差异;ESM 的默认导出是命名导出之一。Tree Shaking:ESM 静态可分析可裁剪,CJS 是动态对象访问基本无法可靠裁剪。工程上:跨模块类型混用时注意 interop,循环依赖尽量靠结构调整规避。

本题考察两大模块体系的机制差异。对比维度是"绑定语义、求值时机、互操作、可分析性",循环依赖与实时绑定是核心考点;能解释 ESM 循环依赖为何通常安全(绑定 + 提升)即展示深层理解。

#
★★

33. 动态 import() 在代码分割、路由懒加载与 SSR 的工程应用

动态 import() 在代码分割、路由懒加载与 SSR 中如何应用?

  • import() 产生独立 chunk 的打包语义
  • 路由级懒加载与预加载策略
  • SSR 双端路径一致性与水合

动态 import() 是代码分割的声明点:打包器把 import() 引用的模块拆为独立 chunk,首屏只加载必要代码,运行时按需拉取。路由懒加载是其最典型应用:React.lazy/动态 import 组件、Vue 的异步组件,路由切换时才加载对应 chunk,配合 prefetch(hover 预取、modulepreload)平衡加载时机;组件级分包、按需引入大依赖(如图表库、编辑器)同理。SSR 应用:服务端渲染时同样执行动态 import,需保证客户端与服务端模块解析结果一致(路径、chunk 清单映射),水合时客户端复用服务端预取结果避免二次拉取;chunk 命名与公共 chunk 拆分(vendor 分离)影响缓存策略。工程注意:动态 import 失败(网络/部署版本)需错误边界兜底;避免把过小模块切分导致请求碎片化。

本题考察动态加载的工程全景。按"分割机制→路由懒加载实践→SSR 一致性"三层展开,预取与错误兜底是易漏点;能说明 SSR 下 chunk 清单(manifest)的作用即展示系统认知。

#
★★

34. 宏任务(setTimeout、setInterval)

setTimeout 与 setInterval 作为宏任务有哪些行为差异与工程注意点?

  • 单次 vs 周期执行与最小延迟钳制
  • 回调执行时长对周期的漂移影响
  • 清理时机与泄漏防范

setTimeout 延迟后执行一次,setInterval 按周期重复执行,二者都走宏任务队列,受嵌套 4ms 钳制与后台节流影响。关键差异:setInterval 的周期是"发起间隔"而非"完成间隔",回调执行时间长于周期时回调会排队积压、甚至跳过(浏览器行为不一),造成漂移与堆叠;setTimeout 递归则每次执行完再排下一次,天然避免堆叠但总周期可能拉长。工程注意:周期任务用递归 setTimeout 模拟以获得可调步长与避免重入;实现心跳/轮询时记录上次执行时间戳校正漂移;clearTimeout/clearInterval 在组件卸载、页面隐藏(visibilitychange)时清理,防止泄漏与无效请求;与 rAF 对比:界面动画优先 rAF,节流类后台任务可退化为间隔更大的轮询。

本题考察宏任务定时器的工程语义。核心是"周期 vs 完成间隔"的漂移机制与递归 setTimeout 的替代价值,清理时机是工程重点;能解释 setInterval 回调重入风险即展示实战经验。

#
★★

35. AbortController 与 AbortSignal 在 fetch、流与定时器的协作取消

AbortController/AbortSignal 如何协作取消 fetch、流与定时器?

  • 同一 signal 传给多个异步操作
  • fetch 中止、流 reader.cancel、定时器清理
  • abort 事件的清理钩子

AbortController 的 signal 可同时传给任意数量的异步操作,abort() 一次触发全链路取消:fetch 收到中止会中断网络请求并拒绝(AbortError);ReadableStream 的 reader.read() 挂起时,signal 中止可取消等待(手动监听 signal 调用 reader.cancel());定时器可监听 abort 事件在取消时执行 clearTimeout,形成"取消时自动清理定时器"的组合。协作模式:把 signal 作为参数穿透封装层,内部所有涉及请求/流/定时器的函数都接收同一 signal 并各自响应 abort;封装时注意移除 abort 监听(一次性事件避免泄漏)与区分 AbortError 与其他错误。工程价值:统一取消入口(用户离开页面、组件卸载、超时)避免悬挂请求与状态竞态。

本题考察取消信号的组合应用。核心是"一个 signal 多消费者"的广播语义与各类 API 的取消方式(fetch 原生、流需 cancel、定时器需监听),监听清理与错误区分是细节考点。

#
★★

36. 事件循环中的 requestAnimationFrame 与浏览器渲染时机的协作机制

requestAnimationFrame 与浏览器渲染时机如何协作?在事件循环中处于什么位置?

  • rAF 回调在渲染前、每帧一次
  • 与屏幕刷新率的对齐与合并
  • 微任务与 rAF 的先后关系

requestAnimationFrame 的回调在浏览器进入渲染阶段前执行,每帧最多一次,与屏幕刷新率(通常 60/120Hz)对齐,是动画与视觉更新的标准通道:浏览器会把同一帧内的多次 rAF 注册合并执行,并在页面不可见/后台时暂停(节省资源)。事件循环位次:宏任务结束并清空微任务后,若帧未过期则执行 rAF 回调,再做样式/布局/绘制;rAF 内修改 DOM 会进入当帧渲染,因此读布局需在 rAF 内主动触发(或读取上次布局值)。与微任务关系:rAF 中排入微任务会在当帧渲染前再次清空;与 setTimeout 相比,rAF 与渲染同频、受帧调度约束,用于动画/滚动联动比定时器精准。工程上:动画循环用 rAF、数据更新后在 rAF 批量写 DOM 可减少重排次数。

本题考察渲染通道的调度位置。核心是"rAF=每帧渲染前的回调、与刷新率对齐、不可见时暂停",微任务先行的先后关系是易错点;能说明 rAF 与重排批量化的配合即展示渲染性能认知。

#
★★

37. 事件循环的 MessageChannel 与 postMessage 在微任务调度的差异

MessageChannel 与 postMessage 在事件循环中的调度有何差异?为何 MessageChannel 曾被用作微任务模拟?

  • 二者都是宏任务级的消息传递
  • MessageChannel 相对 setTimeout 更早执行
  • 历史 polyfill(微任务)的实现原理

window.postMessage 与 MessageChannel 的 message 事件都是宏任务,但优先级与开销不同:MessageChannel 派发消息不经过定时器节流(无 4ms 钳制),且在部分实现中被安排得比 setTimeout 更早执行,因此被用于"尽快执行但非同步"的任务;历史微任务 polyfill(如 MutationObserver 出现前)用 MessageChannel 或 postMessage 模拟微任务队列——借助"消息事件在渲染前、宏任务间尽早处理"的特性。差异要点:postMessage 可跨窗口定向(targetOrigin 限制)且有序列化开销;MessageChannel 建立专用管道、port 可 transfer,同上下文内零成本分发;现代环境应直接用 queueMicrotask,MessageChannel 仅作为兼容回退或专用通道。调度上二者都晚于当前宏任务内微任务。

本题考察消息机制的调度特性。核心是"宏任务级但优先级优于定时器、无钳制"以及历史 polyfill 的原理,现代替代(queueMicrotask)是答题落点;能解释 polyfill 为何选 MessageChannel 即展示历史演进理解。

#
★★

38. Generator 函数与 async generator 在异步迭代(for-await-of)

Generator 函数与 async generator 在异步迭代(for await...of)中如何工作?

  • 同步迭代器与异步迭代器的协议差异
  • async generator 的 yield 挂起与异步恢复
  • for await...of 的消费与提前退出

同步 Generator 实现 [Symbol.iterator](next 返回 {value, done}),由 for...of 消费;async generator(async function*)实现 [Symbol.asyncIterator](next 返回 Promise<{value, done}>),由 for await...of 消费——每次 next() 都产生 Promise,await 其完成后处理值,形成"异步取值"的迭代流。async generator 内部可用 await(在 yield 前后),yield 的值可来自异步操作,天然表达分页拉取、流式数据、事件流等场景;for await...of 中 break/return 会调用 return() 触发清理(如关闭流)。工程实践:Node.js 流、Web Streams 均可转为 async 迭代器消费(for await (const chunk of stream));注意 for await...of 对非异步可迭代对象会尝试包装,而手动消费需逐次 await next() 并处理 done 边界。

本题考察异步迭代协议。核心是"async 迭代器返回 Promise 的 next"与 for await...of 的消费语义,async generator 内 await 的能力是价值点;能说明流式消费与提前退出清理即展示完整掌握。

#
★★

39. Import Maps(

Import Maps 的裸模块说明符别名与 scopes 作用域映射是如何工作的?

  • importmap 的 imports 映射裸说明符
  • scopes 按模块 URL 前缀细分映射
  • 浏览器原生解析与回退

Import Maps 通过

本题考察浏览器原生模块解析。核心是"imports 别名映射 + scopes 前缀细分 + 最长匹配优先"三层规则,声明时机与唯一性是易错点;能结合微前端共享依赖说明工程价值即展示应用认知。

#
★★

40. Import Maps 与构建工具(Vite/Webpack)产物中模块解析的关系及 no-bundler CDN 直载模式的工程边界

Import Maps 与 Vite/Webpack 的产物模块解析有何关系?no-bundler CDN 直载模式的工程边界是什么?

  • 构建工具把裸说明符重写为产物相对路径
  • Import Maps 把解析推迟到运行时
  • CDN 直载的版本锁定、构建依赖与兼容性边界

构建工具(Vite/Webpack/Rollup)在打包阶段把裸说明符静态解析并重写为产物内的相对/带 hash 路径,模块图在构建期固定;Import Maps 则把解析推迟到浏览器运行时,产物保留裸说明符,由 importmap 决定实际 URL——二者是"构建期解析 vs 运行期解析"的两条路线,Vite 的 dev 模式本质是构建期即时重写(依赖预构建 + importmap 辅助)。no-bundler CDN 直载(esm.sh、jspm、unpkg ESM)把裸说明符交给 importmap + CDN 解析:优点是无构建、按需加载、依赖可共享缓存;边界是:依赖版本需显式锁定(importmap 固定版本号)、包需提供 ESM 且声明 sideEffects/导出映射正确、深层依赖图解析复杂、无构建期优化(Tree Shaking 受限、无产物级代码变换)、生产环境需自托管 importmap 与完整性校验(SRI)。工程选型:演示/内部工具直载可行,核心业务仍建议构建产物 + importmap 做运行时共享补充。

本题考察两条解析路线的对比。核心是"构建期 vs 运行期"的解析分工,直载模式的边界落在版本锁定、ESM 供给、优化缺失与安全校验;能给出混合策略(构建 + 运行时共享)即展示工程权衡能力。

#

41. Dynamic Import 的预加载( )在性能优化

如何配合动态 import 做预加载优化?
  • modulepreload 预取并执行模块
  • 与 prefetch/preload 的差异
  • 与动态 import 结合提升跳转响应
指示浏览器提前获取、解析并缓存 ESM 模块(含其依赖图),动态 import() 命中已缓存模块时无需网络等待;相比 rel="prefetch"(低优先级、不解析模块)与 rel="preload"(通用资源),modulepreload 专为模块设计,会预先创建模块图条目。性能优化实践:路由懒加载的组件 chunk 在其即将被导航前(如 hover 菜单、进入视图、空闲时段)注入 modulepreload 链接(或 import() 前手动 fetch),把跳转时延前移到空闲期;打包器(Vite)可自动为入口生成 modulepreload 链(entry chunk 的依赖)。注意:modulepreload 只预取不预执行(模块副作用不会提前运行),多链接按依赖顺序由浏览器调度;滥用会浪费带宽,应结合用户行为预测。

本题考察模块级预加载机制。核心是"预取+预解析+缓存"与动态 import 的命中协作,及其与 prefetch/preload 的差异;能给出"行为预测触发预加载"的实践模式即展示性能优化思路。

#

42. ES Module 的副作用(side-effect-free)

如何理解 ESM 模块的副作用?side-effect-free 在工程中如何声明与利用?

  • 副作用指模块求值时的外部影响
  • package.json 的 sideEffects 字段
  • 纯模块裁剪与副作用模块保留

模块副作用指模块求值过程中对外部状态的影响(修改全局、操作 DOM、注册事件、执行 I/O 等);无副作用的模块(只定义并导出函数/常量)可被安全裁剪。package.json 的 "sideEffects": false 向打包器声明"本包所有模块无副作用",使未引用的模块可整体移除、CSS 类资源通常列出白名单路径("sideEffects": ["*.css"])避免被误删。利用方式:纯函数库声明 sideEffects:false 获得最大裁剪;业务代码保持模块顶层干净(初始化放函数内、避免导入即执行),让 Tree Shaking 生效;polyfill、全局补丁、样式文件必须显式保留。风险:误标 sideEffects:false 会把"导入即生效"的模块删除导致运行时故障,声明前需审计模块顶层行为。

本题考察副作用声明对打包的影响。核心是"副作用定义→sideEffects 字段→裁剪与保留的边界",CSS 白名单与误标风险是工程细节;能解释为何 polyfill 模块必须保留即证明理解到位。

#

43. Promise.withResolvers() 在外部 resolve 异步操作的现代应用

Promise.withResolvers() 在"外部 resolve"异步操作中有哪些现代应用?

  • 解构获取 promise/resolve/reject
  • 事件与回调驱动的落定场景
  • 与传统 executor 闭包对比

Promise.withResolvers() 返回 {promise, resolve, reject},把"创建 Promise"与"控制其落定"解耦,适合落定时机由外部事件/回调决定的场景:事件监听器(用户操作或一次消息触发完成)、桥接回调风格 API(把 callback 包装成 Promise)、手动实现可取消/可暂停流程、信号量等并发原语。相比 new Promise(executor) 中用闭包捕获 resolve 再在异步回调中调用,withResolvers 直接解构出 resolver,代码更清晰且避免了"executor 内同步抛错自动变拒绝"的隐式语义。现代应用还包括:构建外部可控的异步队列(resolve 存储在数据结构中)、与 AbortController 组合实现带取消的桥接。注意:落定只能一次(后续调用无效),未落定前勿让 Promise 逃逸成不可控悬挂。

本题考察 resolver 解耦模式。核心是"创建与落定分离"带来的场景能力(事件驱动、callback 桥接、并发原语),并与 executor 闭包对比优劣;能给出桥接第三方回调的标准写法即展示实用价值。

#

44. Worker 模块(new Worker(url, {type: 'module'}))相较传统脚本的工程取舍

Worker 模块(type: "module")相较传统脚本 Worker 有何工程取舍?

  • 模块化导入导出与静态分析
  • 支持 import/await 的现代能力
  • 兼容性与打包集成考量

new Worker(url, {type: "module"}) 以 ESM 方式加载 Worker:内部可用 import/export 组织代码、复用主线程库代码,支持顶层 await、import.meta.url 等标准 ESM 能力,也允许 importmap 参与解析;传统脚本 Worker 则是单一脚本,代码组织靠 importScripts(同步、无模块语义)。工程取舍:模块 Worker 便于共享类型与纯函数、按依赖图打包(Vite/Webpack 支持多入口 worker 打包)、Tree Shaking 生效;注意点:模块 Worker 中 importScripts 不可用、跨域加载需 CORS、部分旧浏览器/WebView 不支持 type:"module" Worker(需特性检测降级为传统脚本),打包产物 URL 与 hash 管理要纳入构建流程;Worker 内仍无 DOM,通信靠 postMessage/结构化克隆。

本题考察 Worker 的两种加载形态。核心对比"模块化/ESM 能力 vs 传统 importScripts 单脚本",兼容性与打包集成是工程落点;能说明 CORS 与特性检测的边界即展示上线思维。

#

45. 事件循环在浏览器与 Node.js 的差异(nextTick、process.nextTick 优先级)

浏览器与 Node.js 事件循环的差异有哪些?process.nextTick 的优先级如何?

  • 阶段模型 vs 帧模型
  • process.nextTick 队列先于微任务
  • 跨环境编码的注意事项

差异要点:Node.js 事件循环按 timers→pending→poll→check→close 阶段轮询,I/O 回调集中在 poll 阶段,setImmediate 在 check 阶段执行;浏览器按"宏任务→微任务→渲染"帧模型循环,无 setImmediate/nextTick,渲染阶段是浏览器独有。process.nextTick 的优先级高于 Promise 微任务:Node 在每个阶段切换后先执行 nextTick 队列(nextTick 队列与微任务队列分开,nextTick 先清),因此 process.nextTick 递归同样会饿死事件循环(与微任务饥饿同理)。工程注意:跨环境库不要依赖 nextTick/setImmediate,用 queueMicrotask 与标准宏任务保持一致性;Node 中 setImmediate vs setTimeout(0) 的顺序取决于进入 poll 的时机(IO 循环内 setImmediate 先、顶层则定时器先),避免依赖该差异;浏览器侧无"阶段"概念,页面生命周期(可见性、后台节流)影响更显著。

本题考察双环境循环的结构对比。核心是"阶段模型 vs 帧模型"与 nextTick 特殊优先级,setImmediate/setTimeout 的时序不确定性是经典考点;能给出跨环境一致的编码策略即完整。

#

46. Import Maps 与 Module Federation 的演进关系,运行时模块解析、共享依赖治理与微前端远程加载的取舍

Import Maps 与 Module Federation 在运行时模块解析、共享依赖治理与微前端远程加载上有何演进关系与取舍?

  • Module Federation 的构建期运行时映射机制
  • Import Maps 的浏览器原生解析
  • 共享依赖与版本协商的两种路径

Module Federation(Webpack 5)在构建期生成运行时模块映射:把远程模块打包为独立产物并暴露容器(container),运行时通过远程加载器按"范围(scope)+ 模块名"解析,主应用与子应用共享依赖时用"共享作用域"协商版本(谁先加载谁提供,避免双实例)。Import Maps 则是浏览器原生的运行期说明符解析:不参与打包产物生成,只做 URL 映射,共享依赖通过把公共包映射到同一 URL 实现(版本协商靠人工锁定 importmap 条目)。取舍:Module Federation 擅长深度运行时集成(跨应用组件共享、依赖版本智能协商、构建期产物约束),但绑定打包器生态、运行时协议复杂;Import Maps 简单原生、零打包器侵入,但无产物级共享语义、版本管理靠约定。演进方向是两者互补:构建工具产出与 importmap 兼容的产物(Vite 支持),微前端治理中"框架/公共库走 importmap 单实例、业务模块走 federation 远程"。

本题考察两类运行时模块解析机制的定位。核心对比"构建期产物+共享作用域协商 vs 浏览器原生 URL 映射",互补组合是答题落点;能说明共享依赖版本协商的差异即展示微前端治理深度。