异步编程深潜:事件循环、Promise 与 async/await 的底层真相

JavaScript 是单线程的语言,却能够高效地处理网络请求、用户交互、定时器等"耗时"操作,这背后全靠一套精妙的异步编程模型。很多人会用 Promise 和 async/await,但未必清楚事件循环里微任务和宏任务如何调度,更不知道 async 函数不过是 Generator 的语法糖。今天我们就来深潜异步编程,从底层机制到高层抽象,彻底搞懂它。

一、单线程的困境与回调

JavaScript 设计之初是为了操作 DOM、响应用户事件,如果采用多线程同时修改 DOM 会带来复杂的同步问题。因此它选择单线程 + 异步非阻塞模型。

javascript 复制代码
// 同步阻塞(假设 sleep 是阻塞函数)
console.log('开始');
sleep(3000);  // 假想:线程卡住3秒
console.log('结束');  // 3秒后才输出

如果所有操作都同步执行,网络请求或定时器就会让页面"卡死"。解决方案是回调:把后续逻辑封装成函数,等异步操作完成后调用。

javascript 复制代码
console.log('开始');
setTimeout(() => {
  console.log('定时器完成');
}, 3000);
console.log('结束');
// 输出:开始 → 结束 → (3秒后) 定时器完成

回调解决了阻塞问题,但带来了"回调地狱":多层嵌套、错误处理混乱、难以追踪。于是 Promise 应运而生。

二、事件循环:微任务与宏任务

要理解 Promise 和 async/await,必须先掌握 JavaScript 的运行时模型------事件循环(Event Loop)

核心组件

  • 调用栈:执行同步代码的栈结构,函数调用时入栈,返回时出栈。
  • 宏任务队列 :存放宏任务(macro-task),如 setTimeoutsetInterval、I/O、UI 渲染。
  • 微任务队列 :存放微任务(micro-task),如 Promise.thenMutationObserverqueueMicrotask

循环规则

  1. 执行一个宏任务(从队列头部取出)。
  2. 执行过程中产生的微任务,全部依次执行(清空微任务队列)。
  3. 必要时进行 UI 渲染(浏览器约 16.6ms 一次)。
  4. 回到第 1 步,取出下一个宏任务。

关键点 :微任务会在当前宏任务结束、下一个宏任务开始之前全部执行

javascript 复制代码
console.log('1');

setTimeout(() => {
  console.log('2');
  Promise.resolve().then(() => console.log('3'));
}, 0);

Promise.resolve().then(() => {
  console.log('4');
  setTimeout(() => console.log('5'), 0);
});

console.log('6');

// 输出顺序:1, 6, 4, 2, 3, 5

分析

  • 同步代码:1、6 直接输出。
  • 微任务队列:Promise.then 输出 4,内部又添加了宏任务 5。
  • 当前宏任务结束,清空微任务 → 输出 4 结束。
  • 下一个宏任务:setTimeout 回调输出 2,然后它的微任务输出 3。
  • 再下一个宏任务:输出 5。

理解这个顺序是调试异步 bug 的基础。

三、Promise:从零实现一个简版

Promise 是一个状态机,有三种状态:pendingfulfilledrejected。状态一旦改变就不可逆。我们手写一个简化版,就能看清 then 的回调是如何注册和调用的。

javascript 复制代码
class MyPromise {
  constructor(executor) {
    this.state = 'pending';
    this.value = undefined;
    this.callbacks = []; // 存储 then 注册的回调

    const resolve = (value) => {
      if (this.state !== 'pending') return;
      this.state = 'fulfilled';
      this.value = value;
      this.callbacks.forEach(cb => this._handle(cb));
    };

    const reject = (reason) => { /* 类似实现 */ };

    try {
      executor(resolve, reject);
    } catch (e) {
      reject(e);
    }
  }

  then(onFulfilled, onRejected) {
    return new MyPromise((resolve, reject) => {
      this.callbacks.push({
        onFulfilled: () => {
          try {
            const result = onFulfilled(this.value);
            resolve(result);
          } catch (e) {
            reject(e);
          }
        },
        onRejected: () => { /* 类似处理 */ }
      });
    });
  }

  _handle(callback) {
    if (this.state === 'fulfilled') {
      callback.onFulfilled();
    } else if (this.state === 'pending') {
      this.callbacks.push(callback);
    }
  }
}

真正的 Promise 规范还涉及值的穿透、多次 then 链式调用、以及微任务调度(上面简化版是同步调用回调,实际需要用 queueMicrotasksetTimeout 包装)。但核心思想不变:then 只是注册回调,resolve/reject 触发执行

四、async/await:Generator 的语法糖

很多人觉得 async 函数很神奇:函数内部可以写同步风格的代码,却能等待异步结果。实际上,它的底层是 Generator + 自动执行器

Generator 基础

Generator 函数可以暂停和恢复执行:

javascript 复制代码
function* gen() {
  const a = yield 1;
  console.log(a);
  const b = yield 2;
  return b;
}

const it = gen();
console.log(it.next());    // { value: 1, done: false }
console.log(it.next('A')); // 输出 'A', { value: 2, done: false }
console.log(it.next('B')); // { value: 'B', done: true }

yield 可以返回一个值,并等待 next 传参进来恢复执行。这个特性恰好可以用来模拟"等待异步结果"。

模拟 async/await

假如我们想让 generator 能自动执行,并支持 Promise:

javascript 复制代码
function run(generatorFunc) {
  const it = generatorFunc();

  function step(prevResult) {
    const { value, done } = it.next(prevResult);
    if (done) return Promise.resolve(value);
    // 如果 value 是 Promise,等它完成后再继续
    return Promise.resolve(value).then(step);
  }

  return step();
}

// 使用
run(function* () {
  const data1 = yield fetch('/api/1').then(r => r.json());
  const data2 = yield fetch('/api/2').then(r => r.json());
  console.log(data1, data2);
});

这个 run 函数就是一个自动执行器 。而 async/await 正是这种模式的语法糖:async 函数返回 Promise,await 后面跟 Promise,引擎自动在 then 中恢复执行。

javascript 复制代码
// 用 async/await 等价写法
async function fetchData() {
  const data1 = await fetch('/api/1').then(r => r.json());
  const data2 = await fetch('/api/2').then(r => r.json());
  console.log(data1, data2);
}

所以 await 的本质就是把后续代码包装成一个微任务回调,挂载到 Promise 上。

五、常见陷阱与最佳实践

1. 忘记 return 导致并行变串行

javascript 复制代码
// 错误:顺序执行,总耗时 = 请求1 + 请求2
async function bad() {
  const a = await fetch('/api/a');
  const b = await fetch('/api/b');  // 等 a 完成才发 b
}

// 正确:并发执行
async function good() {
  const p1 = fetch('/api/a');
  const p2 = fetch('/api/b');
  const [a, b] = await Promise.all([p1, p2]);
}

2. 不要在循环内使用 await(除非故意串行)

需要并发时用 Promise.allPromise.allSettled

3. 未捕获的 Promise 拒绝

javascript 复制代码
// 危险:错误被吞掉
async function danger() {
  throw new Error('Oops');
}
danger(); // 无报错,但产生一个未处理的拒绝

// 正确:要么 await,要么 .catch
await danger();
// 或 danger().catch(console.error);

现代 Node.js 和浏览器会在未捕获的 Promise 拒绝时打印警告,但最好显式处理。

4. 微任务过多导致 UI 阻塞

javascript 复制代码
// 递归 Promise.then 会一直占用微任务队列,导致无法执行宏任务(包括 UI 渲染)
function loop() {
  Promise.resolve().then(loop);
}
loop();

解决方案:使用 setTimeoutqueueMicrotask 控制。

六、总结

异步编程深潜,我们看到:

  • 事件循环是调度核心,宏任务和微任务的执行顺序决定了代码输出。
  • Promise 本质是状态机,then 注册回调,resolve 触发微任务执行。
  • async/await 是 Generator 的语法糖,让异步代码像同步一样书写。
  • 性能与正确性需要理解并发、错误处理和微任务队列。

掌握这些底层机制,你不仅能写出更可靠的异步代码,还能轻松调试那些"为什么先输出 2 后输出 3"的谜题。下次面试官问"事件循环",你可以自信地从宏任务、微任务讲到 async/await 的实现原理。

异步编程,不再黑盒。


立即进入

相关推荐
像我这样帅的人丶你还7 分钟前
前端监控体系与实践:从错误上报到内存与 GC 观测
前端·javascript·架构
a11177626 分钟前
高斯泼溅 (Gaussian Splatting) 的 Three.js 实现
开发语言·javascript·ecmascript
代码北人生33 分钟前
agent时代,我们都低估了这个 23k Star 的 Claude Code Skills 项目!
javascript
成都渲染101云渲染666634 分钟前
云渲染全面支持3dsMax 2027,高效渲染体验升级
开发语言·前端·javascript
zs宝来了41 分钟前
微前端架构:qiankun 沙箱隔离与样式冲突
前端·javascript·框架
M ? A1 小时前
Vue 的 scoped 样式穿透 React 不支持?用 VuReact 编译就行
前端·javascript·vue.js·react.js·面试·开源·vureact
zs宝来了1 小时前
Vue 3 Composition API:响应式系统与依赖追踪
前端·javascript·框架
村上小树1 小时前
非常简单地学习一下slate.js的原理
前端·javascript
web打印社区1 小时前
[特殊字符] 开源好物:web-print-pdf,让 Web 打印像调用接口一样简单!
前端·javascript·vue.js·electron
大家的林语冰2 小时前
TS 登顶第一语言;JS 最新 Temporal 时间减屎;Node 爆发反 AI 运动;CSS 将支持图片亮暗切换《前端周刊》
前端·javascript·css