Skip to content

JavaScript 异步编程

1. 事件循环机制

题目:说一下事件循环(Event Loop)中宏任务和微任务的执行顺序

难度: ★★

考点: 异步执行机制

答案要点:

事件循环是 JS 处理异步任务的核心机制,遵循先执行同步代码,再清空微任务队列,最后执行宏任务的顺序。

  • 执行流程: 同步代码 -> 微任务(Promise.then, MutationObserver) -> 渲染 -> 宏任务(setTimeout, I/O)
  • 嵌套情况: 宏任务执行过程中产生的微任务会在当前宏任务结束后立即执行
  • Node.js 差异: Node 11 之后与浏览器行为趋同,但仍有 process.nextTick 等特殊阶段
  • 渲染时机: 浏览器通常在一次宏任务执行完且微任务清空后尝试进行 UI 渲染

常见坑:

  • 错误认为 Promise 构造函数内部是异步的
  • 混淆 await 后面的代码执行时机

追问:

  • 如果在微任务里不断递归产生新的微任务,页面会发生什么?

题目:描述一下 JS 的事件循环机制

难度: ★★

考点: 浏览器原理、执行机制

答案要点:

事件循环是浏览器协调 JS 单线程执行与异步任务的机制,通过任务队列的调度实现非阻塞 I/O。

  • 执行栈: 同步代码按顺序入栈执行
  • 微任务(Microtask): Promise.then、MutationObserver、process.nextTick(Node.js),在当前宏任务结束后立即执行
  • 宏任务(Macrotask): setTimeout、setInterval、I/O、UI 渲染,在微任务队列清空后执行
  • 循环流程: 执行一个宏任务 -> 清空微任务队列 -> 检查是否需要渲染 -> 执行下一个宏任务

常见坑:

  • 混淆了 Promise 构造函数(同步执行)与 .then 回调(异步微任务)的执行时机

追问:

  • Node.js 的事件循环与浏览器端有什么主要区别?

2. Promise

题目:Promise 有哪几种状态?如何实现 Promise.all?

难度: ★★

考点: 异步编程

答案要点:

Promise 解决了回调地狱问题,具有 Pending、Fulfilled、Rejected 三种不可逆的状态。

  • Promise.all 特点: 所有 Promise 都成功才成功,只要有一个失败就立即失败
  • 实现逻辑: 返回一个新的 Promise,内部维护一个计数器和结果数组
  • 结果顺序: 通过索引确保输出数组的顺序与输入 Promise 数组顺序一致
  • 边界处理: 处理输入参数不是数组或包含非 Promise 值的情况

代码示例:

javascript
function promiseAll(promises) {
  return new Promise((resolve, reject) => {
    if (!Array.isArray(promises)) {
      return reject(new TypeError('Arguments must be an array'));
    }
    let resolvedCount = 0;
    const results = [];
    promises.forEach((p, index) => {
      Promise.resolve(p).then(res => {
        results[index] = res;
        resolvedCount++;
        if (resolvedCount === promises.length) resolve(results);
      }, err => reject(err));
    });
  });
}

常见坑:

  • 忘记处理 Promise.all 的空数组情况
  • 没考虑到输入数组中普通值的转换

追问:

  • Promise.allSettled 和 Promise.all 的区别是什么?

题目:Promise 的状态有哪些,以及 Promise.all 和 Promise.allSettled 的区别?

难度:

考点: 异步编程、Promise API

答案要点:

Promise 有三种状态,all 和 allSettled 主要区别在于对失败状态的处理逻辑不同。

  • 三种状态: Pending(进行中)、Fulfilled(已成功)、Rejected(已失败)
  • Promise.all: 要求所有任务都成功才返回成功,只要有一个失败就立即进入 reject
  • Promise.allSettled: 无论成功还是失败,都会等所有任务结束,并返回每个任务的结果数组
  • 场景: all 适用于强依赖的一组请求,allSettled 适用于互不影响的并行任务

常见坑:

  • 忘记 Promise 状态一旦改变就不可逆
  • 在 allSettled 的结果中忘记区分 status 字段

追问:

  • 如果 Promise.all 中某个请求失败了,如何保证其他成功的请求结果不被丢弃?

题目:请手写一个简单的 Promise.allSettled 的实现

难度: ★★

考点: Promise 原理、手写代码

答案要点:

Promise.allSettled 会等待所有 Promise 执行完毕,无论成功或失败,并返回包含状态和值的对象数组。

  • 计数器: 记录已完成(fulfilled 或 rejected)的 Promise 数量
  • 结果存储: 按输入顺序存储每个 Promise 的执行结果对象
  • 状态封装: 成功返回 { status: 'fulfilled', value },失败返回 { status: 'rejected', reason }
  • 边界处理: 处理空数组的情况

代码示例:

javascript
function allSettled(promises) {
  return new Promise((resolve) => {
    const results = [];
    let count = 0;
    if (promises.length === 0) resolve([]);
    promises.forEach((p, i) => {
      Promise.resolve(p).then(
        value => {
          results[i] = { status: 'fulfilled', value };
          if (++count === promises.length) resolve(results);
        },
        reason => {
          results[i] = { status: 'rejected', reason };
          if (++count === promises.length) resolve(results);
        }
      );
    });
  });
}

常见坑:

  • 结果数组顺序与输入顺序不一致
  • 未使用 Promise.resolve 包装非 Promise 类型的值

3. async/await

题目:谈谈你对 Promise 的理解,以及 async/await 的执行机制

难度:

考点: JS 异步编程基础

答案要点:

Promise 是 JS 中处理异步操作的对象,通过链式调用解决了回调地狱问题,而 async/await 是基于 Promise 的语法糖。

  • 状态机机制: Promise 有 Pending、Fulfilled、Rejected 三种状态,状态一旦改变不可逆
  • 微任务队列: Promise.then 的回调会被放入微任务队列,在当前宏任务执行完后立即执行
  • 错误捕获: async/await 通过 try-catch 捕获异常,比 Promise.catch 更直观
  • 阻塞特性: await 会暂停当前 async 函数的执行,等待 Promise 解决,但不会阻塞主线程

常见坑:

  • 在 forEach 循环中使用 await 无法达到预期顺序执行的效果
  • 忘记处理 Promise 的 reject 导致 UnhandledPromiseRejection 警告

追问:

  • 如果 Promise.all 中有一个请求失败了,其他请求还会继续执行吗?
  • 如何实现一个超时自动断开的 Promise?

题目:Promise 有哪几种状态?async/await 与它是什么关系?

难度:

考点: 异步语法

答案要点:

Promise 是异步编程的一种解决方案,代表了异步操作的最终完成或失败。

  • 三种状态: Pending(进行中)、Fulfilled(已成功)、Rejected(已失败),状态一旦改变不可逆
  • 关系: async/await 是 Promise 的语法糖,使异步代码在写法上更接近同步代码
  • 错误处理: Promise 使用 .catch,async/await 使用 try...catch

常见坑:

  • 在 async 函数中忘记写 await,导致返回的是一个 Pending 状态的 Promise 对象而非结果

追问:

  • 如果 Promise.all 中有一个请求失败了,整体会是什么状态?

4. 手写异步控制

题目:手写题:实现一个带并发限制的 Promise 调度器

难度: ★★

考点: 异步并发控制

答案要点:

该调度器用于控制同时进行的异步任务数量,防止在多模型调用或大量图片生成时压垮浏览器。

  • 核心逻辑: 维护一个运行中队列和一个等待队列
  • 递归触发: 每个任务完成后,自动从等待队列中取下一个任务执行
  • 状态管理: 记录当前 count,对比 limit

代码示例:

javascript
class Scheduler {
  constructor(limit) {
    this.limit = limit;
    this.count = 0;
    this.queue = [];
  }
  
  async add(fn) {
    if (this.count >= this.limit) {
      await new Promise(resolve => this.queue.push(resolve));
    }
    this.count++;
    const res = await fn();
    this.count--;
    if (this.queue.length > 0) {
      this.queue.shift()();
    }
    return res;
  }
}

// 使用示例
const scheduler = new Scheduler(2);
const timeout = (time) => new Promise(resolve => setTimeout(resolve, time));

const addTask = (time, order) => {
  scheduler.add(() => timeout(time)).then(() => console.log(order));
};

addTask(1000, 1);
addTask(500, 2);
addTask(300, 3);
addTask(400, 4);
// 输出: 2 3 1 4

常见坑:

  • 任务失败时没有正确释放 count 导致死锁

题目:说一下 Fetch API 如何实现请求的中断?

难度:

考点: 异步编程、AbortController

答案要点:

Fetch 本身不直接支持 timeout,需要通过 AbortController 信号量来手动控制请求的终止。

  • 核心步骤: 实例化 AbortController,将 controller.signal 传入 fetch 的 options
  • 触发中断: 调用 controller.abort() 方法
  • 异常捕获: 中断后 fetch 会抛出名为 AbortError 的 DOMException,需在 catch 中识别
  • 场景应用: 用户点击"停止生成"按钮,或组件销毁时清理未完成的 AI 调用

代码示例:

javascript
const controller = new AbortController();
const signal = controller.signal;

fetch('/api/data', { signal })
  .then(response => response.json())
  .catch(err => {
    if (err.name === 'AbortError') {
      console.log('请求已取消');
    }
  });

// 5秒后取消请求
setTimeout(() => controller.abort(), 5000);

常见坑:

  • abort() 调用后,signal 状态不可逆,再次请求需 new 新实例

追问:

  • 如何给 Fetch 封装一个通用的超时控制功能?

5. 流式数据处理

题目:如何在前端处理大模型返回的 Stream 数据时,你会用到哪些 Web API?

难度: ★★

考点: Fetch API、ReadableStream、TextDecoder

答案要点:

主要使用 Fetch API 配合 ReadableStream 接口,通过 TextDecoder 将二进制流转为字符串。

  • ReadableStream: 通过 response.body.getReader() 获取只读流读取器
  • TextDecoder: 处理 UTF-8 编码,支持处理被截断的字节序列(stream: true 模式)
  • 循环读取: 使用 while(true) 配合 reader.read() 递归获取 value 和 done 状态
  • 异常处理: 需要处理网络中断、读取超时以及 reader.cancel() 的手动关闭

代码示例:

javascript
async function readStream() {
  const response = await fetch('/api/stream');
  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  
  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    
    const chunk = decoder.decode(value, { stream: true });
    console.log(chunk);
  }
}

常见坑:

  • 忘记处理 TextDecoder 在字符截断时的拼包问题
  • 未对 reader 进行错误捕获导致内存泄漏

追问:

  • SSE(Server-Sent Events)和 Fetch Stream 相比有什么优缺点?

6. 定时器与执行顺序

题目:setTimeout、setInterval、requestAnimationFrame 的区别?

难度: ★★

考点: 定时器机制

答案要点:

特性setTimeoutsetIntervalrequestAnimationFrame
调用时机延迟执行周期性执行下次重绘前执行
精度最低 4ms可能累积跟随显示器刷新率
节能后台仍运行后台仍运行后台自动暂停
适用场景延迟任务轮询动画

常见坑:

  • setInterval 可能因执行时间过长导致任务堆积
  • setTimeout 嵌套超过 5 层后最小延迟为 4ms

7. 微任务与宏任务完整列表

宏任务(Macrotask)

  • script 整体代码
  • setTimeout / setInterval
  • setImmediate(Node.js)
  • I/O 操作
  • UI 渲染
  • MessageChannel

微任务(Microtask)

  • Promise.then / catch / finally
  • MutationObserver
  • queueMicrotask
  • process.nextTick(Node.js,优先级最高)

执行顺序示例

javascript
console.log('1');

setTimeout(() => console.log('2'), 0);

Promise.resolve().then(() => {
  console.log('3');
  Promise.resolve().then(() => console.log('4'));
});

console.log('5');

// 输出: 1 5 3 4 2