HelloWorld async/await 教程

2026年6月30日 作者:admin

async/await 是基于 Promise 的语法糖,能把异步流控制写得像同步。要学会它,先弄清 Promise、回调地狱的痛点、事件循环及微任务顺序;掌握 await 的阻塞感与并发模式(如 Promise.all、race)、错误处理(try/catch 与未捕获拒绝)、取消与超时(AbortController 或自定义 wrapper),并发限制与重试策略则是工程里常见的优化手段。

HelloWorld async/await 教程

为什么需要 async/await:从回调到 Promise 到 async/await

先回忆一下 JavaScript 异步历史。最初我们用回调(callback),很快出现“回调地狱”:嵌套层层、异常难捕获、控制流混乱。Promise 出现后,把回调扁平化,链式 then/catch 就能清晰表达顺序,但长链还是不如同步代码直观。async/await 作为语法糖,让异步代码像同步那样书写,同时保留 Promise 的非阻塞特性。

一句话理解

  • async function:声明一个函数,返回一个 Promise。
  • await expression:暂停当前 async 函数的执行,直到表达式对应的 Promise 完成(resolve/reject)。

核心概念与事件循环(Event Loop)

理解 async/await 前,要知道事件循环、任务队列与微任务(microtasks)如何工作。简单来说:

  • 宏任务(macrotasks):比如 setTimeout、setInterval、I/O 回调(Node)等。
  • 微任务(microtasks):Promise.then、async/await 的继续执行会排入微任务队列。

当当前执行栈清空时,会先清空所有微任务再去执行下一个宏任务。这也是为什么 await 后面的代码会在当前任务结束后马上执行,而不会被 setTimeout 插队。

示例:微任务与宏任务的执行顺序

// 输出顺序:start -> end -> promise -> timeout
console.log('start');
setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => console.log('promise'));
console.log('end');

如果把 Promise.then 换成 await,效果类似,await 后的代码会作为微任务执行。

从 Hello World 开始:最简 async/await 用法

下面一步步写一个简单的“HelloWorld”异步示例,用来引入常见用法。

function delay(ms) {
  return new Promise(resolve => setTimeout(() => resolve(ms), ms));
}

async function hello() {
  console.log('before');
  const ms = await delay(500);
  console.log('after', ms);
}

hello(); // before -> (0.5s) -> after 500

注意:hello() 返回一个 Promise,如果需要知道它完成,可以用 hello().then(…) 或在另一个 async 函数里 await hello()

错误处理:try/catch 与 Promise.catch

await 会把 Promise 的 reject 转换为异常抛出,因此最常见的模式是用 try/catch 包裹:

async function fetchData() {
  try {
    const res = await fetch('/api/data');
    const json = await res.json();
    return json;
  } catch (err) {
    console.error('请求失败', err);
    throw err; // 视场景决定是否继续抛出
  }
}

如果在外层没有 catch,未处理的 reject 会成为未捕获拒绝(unhandled rejection),Node 或浏览器可以发出警告甚至终止进程。

并发与顺序:如何选择合适的模式

很多初学者以为 await 会并发执行,其实默认是顺序阻塞:await 会等待左边完成再继续。要并发执行多个独立任务,应在等待前先触发所有 Promise,然后再 await 它们。

错误示例(顺序)

for (const url of urls) {
  const r = await fetch(url); // 每个请求等待前一个完成,性能低
  console.log(await r.json());
}

推荐并发示例

// 启动所有请求(立即返回 Promise),再等待所有完成
const promises = urls.map(url => fetch(url).then(r => r.json()));
const results = await Promise.all(promises);

Promise.all 在任一 Promise reject 时会立即 reject;需要容忍部分失败时可以用 Promise.allSettled 或对每个 Promise 添加错误处理。

并发大小限制(限制并发数量)

在真实项目中,直接并发成百上千个请求会导致资源耗尽。常用策略:

  • 批量:把任务分批(chunk)处理。
  • 池(pool):维护固定数量的并发工作者。
// 简易批量示例
async function batchProcess(items, batchSize, worker) {
  const result = [];
  for (let i = 0; i < items.length; i += batchSize) {
    const batch = items.slice(i, i + batchSize).map(worker);
    result.push(...await Promise.all(batch));
  }
  return result;
}

取消与超时:AbortController 与自定义超时

JavaScript 的 Promise 标准并不包含取消功能,但现代 fetch 支持 AbortController。这是实现超时的优雅方式:

function timeout(ms) {
  const controller = new AbortController();
  const id = setTimeout(() => controller.abort(), ms);
  return { signal: controller.signal, clear: () => clearTimeout(id) };
}

async function fetchWithTimeout(url, ms = 5000) {
  const { signal, clear } = timeout(ms);
  try {
    const res = await fetch(url, { signal });
    return await res.json();
  } finally {
    clear();
  }
}

如果使用不支持 AbortController 的库,需要用包装 Promise 的方式实现超时:用 Promise.race 包裹原 Promise 与一个在超时后 reject 的定时器。

高级模式:竞速、优先级、重试

  • Promise.race:用于竞速场景,比如选最快的镜像源。
  • 重试(retry):遇到临时失败时常用指数回退策略。
  • 优先级队列:复杂场景下按优先级调度请求。
async function retry(fn, attempts = 3, delayMs = 200) {
  let err;
  for (let i = 0; i < attempts; i++) {
    try {
      return await fn();
    } catch (e) {
      err = e;
      await new Promise(r => setTimeout(r, delayMs * Math.pow(2, i)));
    }
  }
  throw err;
}

常见陷阱与反模式

  • 在 forEach 中使用 async/await:Array.prototype.forEach 无法等待异步函数,应使用 for…of 或 map + Promise.all。
  • 忘记返回 Promise:async 函数总是返回 Promise,但如果手动封装 Promise,需要确保正确 resolve/reject。
  • 未处理的 Promise 拒绝:会导致不可预期的错误。始终捕获或使用全局处理。
  • 滥用 await:对不需要等待的表达式也 await,会降低并发性能。

调试技巧

  • 在 async 函数里多用 console.log 打时间戳查看顺序。
  • Chrome DevTools 与 Node 的内置调试器能显示异步堆栈(注意有时会被转译工具破坏)。
  • 捕获错误后打印 err.stack,定位来源。

与其它技术的整合

async/await 常见于以下场景:

  • Fetch/axios:网络请求简化。axios 默认返回 Promise。
  • 文件I/O(Node):fs/promises 提供基于 Promise 的 API,适合 await。
  • 数据库操作:多数现代 DB 驱动提供 Promise 接口,await 更易读。

代码示例:结合真实场景的模式

下面举一个稍复杂的例子:从多个镜像源并发获取数据,取最快返回,若失败则重试其它源,最终汇总成功结果并限制并发。

async function fetchFromMirrors(mirrors, limit = 5) {
  const results = [];
  const queue = [...mirrors];

  async function worker() {
    while (queue.length) {
      const url = queue.shift();
      try {
        // 尝试三次
        const data = await retry(() => fetchWithTimeout(url, 3000).then(r => r), 3);
        results.push({ url, data });
      } catch (e) {
        console.warn('镜像失败', url);
      }
    }
  }

  // 启动限量 worker
  await Promise.all(new Array(Math.min(limit, mirrors.length)).fill(null).map(worker));
  return results;
}

性能与内存考虑

一些注意点:

  • 大量未完成的 Promise 会占用内存(尤其是保存了闭包的大对象),应限制并发。
  • 频繁创建短周期 Promise(微任务)在高频场景下会有开销,需评估。
  • 在 Node 中,避免让事件循环长时间被微任务占满,以免影响 I/O 调度。

与生成器(Generators)对比

在 async/await 普及前,生成器 + 协程库(如 co)也能写出“同步风格”的异步代码。async/await 内置在语言层面,更直观,调试更好,性能也更优。生成器仍然有其用途(例如自定义控制流),但大多数场景建议用 async/await。

兼容性与转译(Babel/TypeScript)

现代浏览器与 Node(从较新版本)原生支持 async/await。若需兼容旧环境,可通过 Babel 或 TypeScript 转译,注意转译后的代码可能使用生成器或手写 Promise 垫片,调试堆栈会更复杂。

测试与 Mock

  • 对 async 函数写单元测试时,测试框架通常识别返回 Promise 的测试函数(或直接用 async 测试)。
  • 用 Mock 响应网络请求、数据库调用,避免在测试中依赖真实 I/O。

表格:异步风格比较

风格 优点 缺点
回调 兼容早期平台、简单场景 嵌套难维护,异常处理麻烦
Promise 链 扁平化控制流、链式处理 长链仍不如同步直观
async/await 可读性高、错误处理像同步 易误用导致并发问题,需要理解事件循环

实战小贴士(写给开发过程中的你)

  • 写 await 前先问一句:“这个操作必须按序吗?”如果不是,先触发 Promise 再 await。
  • 对外部接口的调用总是假设可能失败,做好重试与退路。
  • 给异步操作设定合理超时,避免长时间挂起占用资源。
  • 在长期运行的服务里注意内存增长,排查未释放的闭包或未完成的 Promise。

进一步阅读(书名、资料)

  • “You Don’t Know JS”(Kyle Simpson)系列,第二版关于异步与性能的章节。
  • MDN 文档中关于 Promise、async function、AbortController 的条目。

写到这儿,可能会有点信息量——但实战里你会发现,async/await 最有意思的地方不是语法本身,而是它把思路变清楚了:把异步拆成多个小步骤、明确每一步的失败策略和并发边界。接下来你只要多写几次模式化代码:超时包装、并发限制、重试策略、错误上报——这些组合起来就是工业级的可靠异步逻辑。慢慢你会习惯在设计时把异步当成一等公民来思考,然后就更容易在复杂系统里找到平衡,偶尔也会遇到一下奇怪的 race condition,需要再回头看事件循环和微任务队列才恍然大悟——像学任何东西一样,经验会帮你把书面知识变成直觉。

相关文章

了解更多相关内容

HelloWorld智能翻译软件 与世界各地高效连接