async/await 到底怎么工作的?

0 阅读7分钟

async/await 这东西,说难不难,说简单也不简单。

很多人用了很久,但真要解释"它底层怎么跑的",就开始含糊了。这篇文章就是要把这个说清楚。

先从一个问题开始

你有没有想过,这段代码为什么能"暂停"?

async function fetchUser() {
  const res = await fetch('/api/user');
  const data = await res.json();
  return data;
}

JavaScript 是单线程的,理论上不能"等"——一旦卡住,整个页面就冻结了。但 await 明明就在等,而且等的时候页面还能正常响应。

这是怎么做到的?

先搞懂 Promise

async/await 是 Promise 的语法糖,所以得先知道 Promise 是什么。

Promise 本质上就是一个状态机,三种状态:

  • pending:等待中
  • fulfilled:成功了
  • rejected:失败了

状态只能从 pending 变成另外两种,而且不可逆。

const p = new Promise((resolve, reject) => {
  setTimeout(() => resolve('done'), 1000);
});

p.then(result => console.log(result)); // 1秒后打印 "done"

关键点:.then() 里的回调不是立刻执行的,它被放进了微任务队列,等当前同步代码跑完再执行。

事件循环:JavaScript 的调度核心

要理解 async/await,必须知道事件循环(Event Loop)。

JavaScript 的执行模型大概是这样:

调用栈(Call Stack)
    ↓ 同步代码在这里执行
    
微任务队列(Microtask Queue)
    ↓ Promise.then、queueMicrotask 等
    
宏任务队列(Macrotask Queue)
    ↓ setTimeoutsetInterval、I/O 等

执行顺序:

  1. 跑完调用栈里的同步代码
  2. 清空微任务队列(全部跑完)
  3. 取一个宏任务执行
  4. 再清空微任务队列
  5. 循环往复

这就是为什么 Promise 的回调比 setTimeout 先执行:

console.log('1');

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

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

console.log('4');

// 输出顺序:1 → 4 → 3 → 2

async/await 的真面目

async 函数本质上是一个返回 Promise 的函数。

async function foo() {
  return 42;
}

// 等价于
function foo() {
  return Promise.resolve(42);
}

await 则是暂停当前 async 函数的执行,等 Promise 完成后再继续。

但"暂停"不是真的停住——它只是把后面的代码包成一个回调,注册到 Promise 的 .then() 里,然后把控制权还给调用者。

用伪代码理解:

async function fetchUser() {
  const res = await fetch('/api/user');
  const data = await res.json();
  return data;
}

// 大概等价于
function fetchUser() {
  return fetch('/api/user').then(res => {
    return res.json().then(data => {
      return data;
    });
  });
}

所以 await 并没有阻塞线程,它只是把"等待之后的逻辑"推迟到 Promise 完成时执行。

生成器:async/await 的前身

async/await 的实现原理和生成器(Generator)密切相关。

生成器可以在函数执行中途暂停,然后从暂停的地方继续:

function* gen() {
  console.log('step 1');
  yield;
  console.log('step 2');
  yield;
  console.log('step 3');
}

const g = gen();
g.next(); // 打印 "step 1",暂停
g.next(); // 打印 "step 2",暂停
g.next(); // 打印 "step 3",结束

把生成器和 Promise 结合起来,就能实现"等 Promise 完成后继续执行"的效果。这正是 async/await 在语言层面做的事。

早期没有 async/await 时,社区用 co 这个库来实现类似效果:

// co 库的用法(历史产物,了解即可)
co(function* () {
  const res = yield fetch('/api/user');
  const data = yield res.json();
  return data;
});

async/await 就是把这个模式内置到语言里了。

一个完整的执行过程

来看这段代码,逐步分析执行顺序:

async function main() {
  console.log('A');
  const result = await Promise.resolve('hello');
  console.log('B', result);
}

console.log('start');
main();
console.log('end');

执行过程:

  1. 打印 start
  2. 调用 main(),打印 A
  3. 遇到 await,把 console.log('B', result) 注册为微任务,main 函数暂停,控制权返回
  4. 打印 end
  5. 同步代码跑完,清空微任务队列
  6. 打印 B hello

输出:start → A → end → B hello

很多人会以为是 start → A → B hello → end,这是个常见误区。

错误处理

async/await 的错误处理比 Promise 链直观很多:

// Promise 链写法
fetch('/api/user')
  .then(res => res.json())
  .then(data => console.log(data))
  .catch(err => console.error(err));

// async/await 写法
async function fetchUser() {
  try {
    const res = await fetch('/api/user');
    const data = await res.json();
    console.log(data);
  } catch (err) {
    console.error(err);
  }
}

try/catch 能捕获 await 抛出的错误,包括网络错误、JSON 解析错误等。

有一个细节要注意:如果 async 函数里没有 try/catch,错误会变成 rejected 的 Promise,需要在调用处处理:

async function fetchUser() {
  const res = await fetch('/api/user'); // 如果失败,会抛出
  return await res.json();
}

// 调用处处理
fetchUser().catch(err => console.error(err));
// 或者
try {
  await fetchUser();
} catch (err) {
  console.error(err);
}

并发执行

await 是串行的,一个等完再等下一个。如果两个请求互不依赖,串行就浪费时间了:

// 串行:总耗时 = 请求1时间 + 请求2时间
async function serial() {
  const user = await fetchUser();    // 等 500ms
  const posts = await fetchPosts();  // 再等 300ms
  // 总共 800ms
}

// 并发:总耗时 = max(请求1时间, 请求2时间)
async function parallel() {
  const [user, posts] = await Promise.all([
    fetchUser(),   // 同时发出
    fetchPosts()   // 同时发出
  ]);
  // 总共 500ms
}

Promise.all 同时发起多个请求,等全部完成后返回结果数组。

手搓 async/await:从零实现一遍

光看原理不过瘾,自己实现一遍才真的懂。

第一步:手写一个 Promise

先把 Promise 的核心逻辑实现出来:

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 => cb.onFulfilled(value));
    };

    const reject = (reason) => {
      if (this.state !== 'pending') return;
      this.state = 'rejected';
      this.value = reason;
      this.callbacks.forEach(cb => cb.onRejected(reason));
    };

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

  then(onFulfilled, onRejected) {
    // 返回新的 Promise,支持链式调用
    return new MyPromise((resolve, reject) => {
      const handle = (fn, val) => {
        // 用 queueMicrotask 模拟微任务
        queueMicrotask(() => {
          try {
            const result = fn(val);
            // 如果返回的也是 Promise,等它完成
            if (result instanceof MyPromise) {
              result.then(resolve, reject);
            } else {
              resolve(result);
            }
          } catch (err) {
            reject(err);
          }
        });
      };

      if (this.state === 'fulfilled') {
        handle(onFulfilled, this.value);
      } else if (this.state === 'rejected') {
        handle(onRejected, this.value);
      } else {
        // 还在 pending,先存起来等 resolve/reject 触发
        this.callbacks.push({
          onFulfilled: val => handle(onFulfilled, val),
          onRejected: val => handle(onRejected, val),
        });
      }
    });
  }

  catch(onRejected) {
    return this.then(null, onRejected);
  }

  static resolve(value) {
    return new MyPromise(resolve => resolve(value));
  }

  static reject(reason) {
    return new MyPromise((_, reject) => reject(reason));
  }
}

验证一下:

new MyPromise((resolve) => {
  setTimeout(() => resolve('hello'), 500);
}).then(val => {
  console.log(val); // 500ms 后打印 "hello"
  return val + ' world';
}).then(val => {
  console.log(val); // 打印 "hello world"
});

第二步:用生成器模拟 await

生成器的 yield 可以暂停函数,这和 await 的行为一模一样。

先写一个"执行器",让生成器自动跑完:

function runGenerator(genFn) {
  return new MyPromise((resolve, reject) => {
    const gen = genFn(); // 拿到生成器对象

    function step(nextFn) {
      let result;
      try {
        result = nextFn(); // 执行到下一个 yield
      } catch (err) {
        return reject(err);
      }

      if (result.done) {
        // 生成器跑完了,resolve 最终值
        return resolve(result.value);
      }

      // result.value 是 yield 右边的 Promise
      // 等它完成后,把结果传回生成器继续执行
      MyPromise.resolve(result.value).then(
        val => step(() => gen.next(val)),      // 成功:继续
        err => step(() => gen.throw(err))      // 失败:抛错
      );
    }

    step(() => gen.next()); // 启动
  });
}

用起来是这样的:

// 模拟一个异步请求
function fakeRequest(url) {
  return new MyPromise(resolve => {
    setTimeout(() => resolve(`data from ${url}`), 300);
  });
}

// 用生成器写"同步风格"的异步代码
runGenerator(function* () {
  console.log('开始请求');
  const user = yield fakeRequest('/api/user');
  console.log('用户数据:', user);
  const posts = yield fakeRequest('/api/posts');
  console.log('文章数据:', posts);
  return '全部完成';
}).then(result => {
  console.log(result);
});

// 输出:
// 开始请求
// 用户数据: data from /api/user
// 文章数据: data from /api/posts
// 全部完成

这就是 co 库的核心逻辑,也是 async/await 的底层原理。

第三步:封装成 async/await 的形式

把上面的 runGenerator 包一层,就得到了 async 函数的效果:

function myAsync(genFn) {
  return function(...args) {
    return runGenerator(function* () {
      return yield* genFn(...args); // 代理生成器
    });
  };
}

用法:

const fetchUser = myAsync(function* () {
  const res = yield fakeRequest('/api/user');
  const data = yield fakeRequest('/api/parse');
  return data;
});

// 和真正的 async 函数用法一样
fetchUser().then(data => console.log(data));

第四步:加上错误处理

真实的 async/await 支持 try/catch,生成器也支持:

runGenerator(function* () {
  try {
    const data = yield MyPromise.reject(new Error('请求失败'));
    console.log(data); // 不会执行
  } catch (err) {
    console.log('捕获到错误:', err.message); // 打印 "捕获到错误: 请求失败"
  }
});

当 Promise reject 时,执行器调用 gen.throw(err),把错误抛进生成器,生成器里的 try/catch 就能捕获到。

完整实现汇总

// 1. 简版 Promise
class MyPromise { /* 见上文 */ }

// 2. 生成器执行器(async/await 的核心)
function runGenerator(genFn) {
  return new MyPromise((resolve, reject) => {
    const gen = genFn();
    function step(nextFn) {
      let result;
      try { result = nextFn(); } catch (err) { return reject(err); }
      if (result.done) return resolve(result.value);
      MyPromise.resolve(result.value).then(
        val => step(() => gen.next(val)),
        err => step(() => gen.throw(err))
      );
    }
    step(() => gen.next());
  });
}

// 3. async 函数工厂
function myAsync(genFn) {
  return function(...args) {
    return runGenerator(() => genFn(...args));
  };
}

// 4. 使用示例
const main = myAsync(function* () {
  try {
    const user = yield fakeRequest('/api/user');
    const posts = yield fakeRequest('/api/posts');
    return { user, posts };
  } catch (err) {
    console.error('出错了:', err);
  }
});

main().then(result => console.log('结果:', result));

跑一遍这段代码,你就真的理解 async/await 了。


总结

async/await 的工作原理,核心就三点:

  1. async 函数返回 Promise,函数内部的 return 值会被包成 resolved 的 Promise
  2. await 暂停函数执行,把后续代码注册为微任务,把控制权还给调用者,不阻塞线程
  3. 底层依赖事件循环,微任务队列保证了 await 之后的代码在当前同步任务完成后立即执行

手搓一遍的收获:

  • Promise 的链式调用靠的是每次 .then() 返回新 Promise
  • 生成器的 yield 就是 await 的前身
  • 执行器负责"驱动"生成器一步步跑完
  • 错误通过 gen.throw() 注入生成器,被 try/catch 捕获

理解了这些,async/await 的各种"奇怪行为"就都能解释了。