异步编程深潜:事件循环、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 的实现原理。

异步编程,不再黑盒。


立即进入

相关推荐
川冰ICE22 分钟前
TypeScript装饰器与元编程实战
前端·javascript·typescript
AI砖家33 分钟前
Vue3组件传参大全,各种传参方式的对比
前端·javascript·vue.js
希望永不加班34 分钟前
var局部变量类型推断的利弊
java·服务器·前端·javascript·html
threelab1 小时前
Three.js 3D 地图可视化 | 三维可视化 / AI 提示词
前端·javascript·人工智能·3d·着色器
失眠的咕噜2 小时前
PDA 安卓设备上传多张图片
android·前端·javascript
掰头战士2 小时前
深入了解JS原型及原型继承链机制
javascript
一只叁木Meow2 小时前
电商 SKU 选择器:用算法实现优雅的用户交互
前端·javascript·算法
代码煮茶2 小时前
Vue3 Mock 数据实战 | 用 Mockjs + vite-plugin-mock 搭建前端独立开发环境
javascript·vue.js
JieE2122 小时前
反转链表:从双指针到递归,吃透链表反转的核心逻辑
javascript·算法
码银3 小时前
在若依中如何新建一个模块(图文教程)
java·javascript