现代异步 async/await

用同步的写法,处理异步的事

🎯 学习目标

学完本章,你将能够:

  • 能够使用 async/await 重写 Promise 链式代码,提升可读性
  • 能够使用 try/catch 捕获 async 函数中的错误
  • 能够区分串行和并行执行,合理使用 Promise.all 优化性能
  • 能够使用 for await...of 遍历异步可迭代对象
  • 能够实现简单的并发控制,限制同时执行的异步任务数量

对比一下

两种写法,同一个功能,哪个更易读?

// Promise 链式写法
function getUser(id) {
    return fetch(`/api/users/${id}`)
        .then(res => res.json())
        .then(user => fetch(`/api/orders/${user.id}`))
        .then(res => res.json())
        .catch(err => console.error(err));
}

// async/await 写法
async function getUser(id) {
    try {
        let res = await fetch(`/api/users/${id}`);
        let user = await res.json();
        let orders = await fetch(`/api/orders/${user.id}`);
        return await orders.json();
    } catch (err) {
        console.error(err);
    }
}

第二种写法读起来就像同步代码——一行一行往下走,逻辑清晰。这就是 async/await 的魅力。

它不是什么新魔法,本质上是 Promise 的语法糖。但这个糖实在太甜了。

Part 1: async 函数

加个关键字,普通函数变成 Promise 工厂

对比开场的两种写法,async/await 版本读起来就像同步代码——没有 .then(),没有嵌套。秘密就在 async 关键字上:把它加在函数声明前面,函数就自动返回 Promise。这一部分先搞懂 async 函数的基本行为,为后面的 await 打好基础。

async 函数

加个关键字,普通函数变成 Promise 工厂

async 函数是使用 async 关键字声明的函数。它总是返回一个 Promise。即使你返回一个普通值,也会被自动包装成 Promise.resolve(value)。

说人话:async 就像给函数贴了个标签,告诉 JS 引擎"这个函数里面有异步操作,请用 Promise 的方式对待它"。

这个比喻只是便于记忆。实际上 async 做的事情就是改写函数的返回值处理逻辑,在引擎层面并没有什么"标签"。

async function greet() {
    return "Hello!"; // 等同于 return Promise.resolve("Hello!")
}
greet().then(msg => console.log(msg)); // "Hello!"

// async 箭头函数
const greet2 = async () => "Hello!";

// async 方法
const obj = {
    async fetchData() {
        return { data: "ok" };
    }
};

// 抛出错误 → rejected Promise
async function fail() {
    throw new Error("出错了");
}
fail().catch(err => console.log(err.message)); // "出错了"

Part 2: await

暂停函数,等 Promise 完成再继续

async 函数本身只是返回 Promise,真正让代码"看起来同步"的是 await。它会在 Promise 完成前暂停当前函数,拿到结果后再继续往下走。但注意:暂停的不是整个程序,只是这一个函数,外部代码照常运行。理解了"暂停不等于阻塞",才算真正掌握了 await。

await 暂停

等一等,结果来了再继续

await 只能在 async 函数中使用(ES2022 顶层 await 除外)。它会暂停当前函数的执行,等待 Promise 完成,然后返回结果。

async function fetchUser() {
    let response = await fetch("/api/users/1");
    let user = await response.json();
    console.log(user);
    return user;
}

// await 后面不一定是 Promise
async function example() {
    let value = await 42;    // 等同于 await Promise.resolve(42)
    let name = await "hello";
    console.log(value, name);
}
顶层 await:在 ES 模块中可以直接在顶层使用 await,不需要包裹在 async 函数中。但普通 JS 文件中依然需要 async 包装。

await 流程

暂停 ≠ 阻塞

图:await 执行流程——async 函数的暂停与恢复机制

关键:await 只暂停 async 函数内部,不会阻塞整个线程。外部代码照常运行。

IIFE 包装

没有 async 函数?自己造一个

// 在不支持顶层 await 的环境中,用 IIFE 包裹
(async () => {
    let data = await fetchData();
    console.log(data);
})();

IIFE(Immediately Invoked Function Expression)就是"立即执行函数表达式"。定义完立刻调用,不需要名字。

Part 3: 错误处理

异步出错怎么办?四种模式各有适用场景

await 拿到的是 Promise 的结果,但如果 Promise 被拒绝了呢?不处理就直接崩溃。同步代码用 try/catch,异步也一样——但还有更灵活的模式:单独捕获每个 await,或者用包装函数避免到处写 try/catch。这一部分给出四种错误处理方式,帮你根据场景选择最合适的。

try/catch

最直观的错误处理方式

async function getUser(id) {
    try {
        let response = await fetch(`/api/users/${id}`);
        if (!response.ok) {
            throw new Error(`HTTP ${response.status}`);
        }
        let user = await response.json();
        return user;
    } catch (error) {
        console.error("获取用户失败:", error.message);
        return null;
    } finally {
        console.log("请求完成");
    }
}

模式一:try/catch 包裹整个函数体。适合简单场景,所有错误统一处理。

错误处理模式

四种方式,各有适用场景

// 模式一:try/catch 包裹全部
async function mode1() {
    try {
        let a = await step1();
        let b = await step2(a);
        return b;
    } catch (e) {
        console.error("出错了:", e);
    }
}

// 模式二:每个 await 单独 try/catch
async function mode2() {
    let a = await step1().catch(e => {
        console.error("step1 失败:", e);
        return defaultValue;
    });
    let b = await step2(a).catch(e => {
        console.error("step2 失败:", e);
        return defaultValue;
    });
    return b;
}

// 模式三:包装函数(不写 try/catch)
async function mode3() {
    let [a, err1] = await safeExecute(step1());
    if (err1) return handleError(err1);
    let [b, err2] = await safeExecute(step2(a));
    if (err2) return handleError(err2);
    return b;
}
async function safeExecute(promise) {
    try { return [await promise, null]; }
    catch (e) { return [null, e]; }
}

// 模式四:catch 链式
async function mode4() {
    let a = await step1().catch(handleError);
    let b = await step2(a).catch(handleError);
    return b;
}

Part 4: 并行优化

没有依赖关系的操作,别排队,一起跑

连续写两个 await,它们是串行执行的——第一个完成才开始第二个。但如果两个请求互不依赖,串行等于白等一倍时间。用 Promise.all 配合 await,可以让多个异步操作并行跑。这一部分讲清楚串行和并行的区别,以及如何控制并发数量防止"一次性发太多请求"。

串行与并行

同样的操作,时间差一半

// 串行(慢)——一个接一个
async function serial() {
    const a = await fetchA(); // 等待 1s
    const b = await fetchB(); // 再等 1s,总共 2s
    return [a, b];
}

// 并行(快)——同时发起
async function parallel() {
    const [a, b] = await Promise.all([
        fetchA(), // 同时发起
        fetchB()  // 同时发起,总共 1s
    ]);
    return [a, b];
}

时间线对比

可视化串行与并行的执行差异

图:串行与并行时间线对比——2 秒 vs 1 秒
// 按需并行
async function optimized() {
    // 不互相依赖的请求并行
    let [users, config] = await Promise.all([
        fetchUsers(),
        fetchConfig()
    ]);
    // 依赖前面的结果
    let details = await fetchDetails(users[0].id);
    return { users, config, details };
}

批量并发

控制并发数量,既快又稳

async function batchProcess(items, concurrency = 5) {
    let results = [];
    for (let i = 0; i < items.length; i += concurrency) {
        let batch = items.slice(i, i + concurrency);
        let batchResults = await Promise.all(
            batch.map(item => processItem(item))
        );
        results.push(...batchResults);
    }
    return results;
}

每批发 5 个请求,等这 5 个全部完成后再发下一批。避免一次性发 1000 个请求导致服务器崩溃。

Part 5: 顶层 await

模块顶层直接 await,不用再包一层

以前用 await 必须包在 async 函数里,顶部代码想 await 就得写一个 IIFE。ES2022 的顶层 await 终于免去了这个麻烦:在 ES 模块顶层直接 await,模块会等它完成后再暴露给导入方。适合配置加载、条件导入等场景。

顶层 await

模块级别直接等待

// config.mjs
const config = await loadConfig();
export default config;

// app.mjs
import config from './config.mjs';
// 等 config 加载完成后才执行后续代码
console.log(config);

// 条件加载
const adapter = await (isNode
    ? import('./node-adapter.js')
    : import('./browser-adapter.js'));
限制:顶层 await 只能在 ES 模块中使用(.mjs 文件或在 package.json 中设置 "type": "module")。CommonJS 模块不支持。

Part 6: 异步迭代

逐个消费异步数据流

分页接口每次返回一页数据,想拿全部数据怎么办?for await...of 配合异步生成器,可以像遍历普通数组一样遍历异步数据源。每拿一页处理一页,不需要一次性加载全部,内存占用恒定。在文件逐行读取、流式处理等场景中非常好用。

for await

逐个消费异步数据流

// 异步生成器
async function* fetchPages(url) {
    let page = 1;
    while (true) {
        let res = await fetch(`${url}?page=${page}`);
        let data = await res.json();
        if (data.items.length === 0) return;
        yield data.items;
        page++;
    }
}

// 用 for await...of 消费
async function getAllItems() {
    let all = [];
    for await (let items of fetchPages("/api/items")) {
        all.push(...items);
    }
    return all;
}
// 逐行读取文件(Node.js)
const fs = require('fs/promises');
const readline = require('readline');

async function readFileLines(path) {
    const stream = fs.createReadStream(path);
    const rl = readline.createInterface({ input: stream });
    for await (let line of rl) {
        console.log(line);
    }
}

常见 async 错误

await 只能在 async 函数内使用。 如果在普通函数中写 await,会直接报 SyntaxError。

问题一:忘记 await

// 错误:拿到的是 Promise,不是结果
async function wrong() {
    let user = fetchUser(1); // 忘记 await
    console.log(user); // Promise { <pending> }
}

// 正确
async function right() {
    let user = await fetchUser(1);
    console.log(user); // { name: "张三", ... }
}

问题二:循环中的串行陷阱

// 慢:串行执行(每个请求等上一个完成)
for (let url of urls) {
    let data = await fetch(url);
    console.log(data);
}

// 快:并行执行
let promises = urls.map(url => fetch(url));
let results = await Promise.all(promises);

并发控制

限制同时运行的 Promise 数量

async function limitConcurrency(tasks, limit) {
    const results = [];
    const executing = new Set();

    for (const task of tasks) {
        // 如果正在执行的数量达到上限,等待任意一个完成
        if (executing.size >= limit) {
            await Promise.race(executing);
        }

        // 创建 Promise 并加入执行集合
        const p = Promise.resolve().then(() => task());
        const wrapped = p.then(result => {
            executing.delete(wrapped);
            return result;
        });
        executing.add(wrapped);
        results.push(wrapped);
    }

    // 等待所有剩余任务完成
    return Promise.all(results);
}

// 使用场景:批量请求不压垮服务器
const urls = Array.from({ length: 100 }, (_, i) =>
    `https://api.example.com/items/${i}`
);

async function fetchAll() {
    const tasks = urls.map(url => () => fetch(url));
    // 最多同时 5 个请求
    const responses = await limitConcurrency(tasks, 5);
    const data = await Promise.all(responses.map(r => r.json()));
    console.log(`获取了 ${data.length} 条数据`);
}
易错点:Promise.race 只能告诉我们"有一个完成了",但不会返回它在数组中的位置。用 Set 管理 executing 并在完成时 delete,才能正确追踪剩余并发数。

异步迭代实战

用异步生成器优雅处理分页

// 异步生成器:自动翻页,逐页 yield 数据
async function* paginate(url, transform) {
    let page = 1;
    let hasMore = true;

    while (hasMore) {
        const res = await fetch(`${url}?page=${page}`);
        const data = await res.json();

        if (data.items.length === 0) {
            hasMore = false;
            return;
        }

        // 用 transform 函数处理每一页数据
        yield transform ? transform(data.items) : data.items;
        page++;
    }
}

// 消费数据流:逐页处理,内存友好
async function syncAllUsers() {
    const batchSize = 50;
    let total = 0;

    for await (let users of paginate('/api/users', items =>
        items.filter(u => u.active).map(u => ({
            id: u.id,
            name: u.name
        }))
    )) {
        await batchSave(users);
        total += users.length;
        console.log(`已同步 ${total} 条用户`);
    }

    console.log(`同步完成,共 ${total} 条`);
}

// 模拟批量保存
async function batchSave(users) {
    await new Promise(r => setTimeout(r, 100)); // 模拟 IO
}

异步生成器的优势:不需要一次性加载所有数据。每处理完一页才请求下一页,内存占用恒定。配合 transform 函数可以在 yield 前对数据做清洗、过滤、映射。

Part 7: 进阶实战

把 async/await、错误处理、并发控制组合起来

前面的知识都学完了,现在要把它们组合起来解决真实问题:用 async/await 重写 Promise 链、给请求加超时控制、实现一个带并发限制的批量处理器。三道练习题从易到难,做完就算真正出师了。

练习一

任务:用 async/await 重写下面的 Promise 链。

function getData() {
    return fetch("/api/data")
        .then(res => res.json())
        .then(data => {
            console.log(data);
            return data;
        })
        .catch(err => console.error(err));
}
// 你的 async/await 版本:

参考答案

async function getData() {
    try {
        let res = await fetch("/api/data");
        let data = await res.json();
        console.log(data);
        return data;
    } catch (err) {
        console.error(err);
    }
}

练习二

任务:实现一个 withTimeout(fetch(url), 3000) 函数,如果请求 3 秒内没完成就抛出超时错误。

// 提示:Promise.race + setTimeout
// 你的代码:

参考答案

async function withTimeout(promise, ms) {
    let timeout = new Promise((_, reject) =>
        setTimeout(() => reject(new Error("超时")), ms)
    );
    return Promise.race([promise, timeout]);
}

练习三

任务:实现 promiseMap(items, fn, concurrency)——对数组中的每个元素执行异步函数 fn,但限制并发数量为 concurrency。返回所有结果的数组(保持原顺序)。

// 提示:维护一个执行中的 Promise 集合
// 每当有空位就启动下一个任务
// 最后 Promise.all 保证顺序
// 你的代码:

参考答案

async function promiseMap(items, fn, concurrency) {
    const results = new Array(items.length);
    let nextIndex = 0;

    async function worker() {
        while (nextIndex < items.length) {
            const index = nextIndex++;
            results[index] = await fn(items[index], index);
        }
    }

    // 启动 concurrency 个 worker 并行执行
    const workers = Array.from(
        { length: Math.min(concurrency, items.length) },
        () => worker()
    );

    await Promise.all(workers);
    return results;
}

// 测试
async function test() {
    const items = [1, 2, 3, 4, 5, 6, 7, 8];
    const results = await promiseMap(
        items,
        async (n) => {
            await new Promise(r => setTimeout(r, 100));
            return n * 2;
        },
        3 // 最多 3 个并发
    );
    console.log(results); // [2, 4, 6, 8, 10, 12, 14, 16]
}
test();

📝 要点总结

本章核心知识点

async 函数

声明异步函数,自动返回 Promise

await 暂停

等待 Promise resolve 后继续执行

错误处理

try/catch 捕获异步错误

串行与并行

await 串行,Promise.all 并行

并发控制

限制同时执行的异步任务数量

下一章

async/await 让异步代码变得优雅。下一章我们探索一个更高级的特性:生成器与迭代器——能暂停、能恢复的函数。

1 / X