Async/Await 的本质:它是如何基于 Generator 和协程(Coroutine)实现的语法糖

各位同仁,各位技术爱好者,大家好。

今天,我们将深入探讨 JavaScript 异步编程的核心演进,特别是 async/await 这对语法糖的本质。在日常开发中,我们频繁使用 async/await 来编写简洁、易读的异步代码,但它究竟是如何工作的?它与我们熟悉的 Promise 有何关联?它又是如何利用 JavaScript 语言特性中的 Generator 和协程(Coroutine)思想来实现的呢?

本次讲座,我将带大家从回调地狱出发,逐步深入,层层揭开 async/await 的神秘面纱。我们将看到,它并非魔法,而是基于一系列巧妙的抽象和转换,最终为我们提供了接近同步代码的异步编程体验。


一、异步的困境:从回调地狱说起

在 JavaScript 的早期,异步操作主要通过回调函数(Callbacks)来处理。当一个耗时操作(如网络请求、文件读写)完成时,它会调用一个预先定义好的函数来处理结果。这种方式在简单的场景下尚可接受,但一旦异步操作之间存在依赖关系,或者需要进行多次连续的异步调用,问题便随之而来。

考虑一个典型的场景:我们需要依次获取用户数据、根据用户ID获取订单数据,再根据订单ID获取商品详情。

// 模拟异步 API 调用
function getUser(userId, callback) {
    setTimeout(() => {
        console.log(`Getting user for ID: ${userId}`);
        const user = { id: userId, name: 'Alice', orderIds: ['order_1', 'order_2'] };
        callback(null, user);
    }, 500);
}

function getOrders(orderIds, callback) {
    setTimeout(() => {
        console.log(`Getting orders for IDs: ${orderIds.join(', ')}`);
        const orders = orderIds.map(id => ({ id: id, productId: `product_${id.split('_')[1]}` }));
        callback(null, orders);
    }, 700);
}

function getProduct(productId, callback) {
    setTimeout(() => {
        console.log(`Getting product for ID: ${productId}`);
        const product = { id: productId, name: `Product ${productId.split('_')[1]}`, price: 100 };
        callback(null, product);
    }, 300);
}

// 实际业务逻辑
function fetchUserDataAndProducts_CallbackHell(userId) {
    getUser(userId, (err, user) => {
        if (err) {
            console.error('Error getting user:', err);
            return;
        }
        console.log('User:', user.name);

        getOrders(user.orderIds, (err, orders) => {
            if (err) {
                console.error('Error getting orders:', err);
                return;
            }
            console.log('Orders found:', orders.length);

            // 假设我们只关心第一个订单的商品
            if (orders.length > 0) {
                const firstOrderProductId = orders[0].productId;
                getProduct(firstOrderProductId, (err, product) => {
                    if (err) {
                        console.error('Error getting product:', err);
                        return;
                    }
                    console.log('First order product:', product.name, product.price);
                    console.log('All data fetched using Callbacks.');
                });
            } else {
                console.log('No orders found.');
            }
        });
    });
}

console.log('--- Starting Callback Hell Example ---');
fetchUserDataAndProducts_CallbackHell(123);
console.log('--- Callback Hell Example Initiated ---'); // 这句话会先打印,体现异步非阻塞

这段代码展示了经典的“回调地狱”(Callback Hell)或“厄运金字塔”(Pyramid of Doom)。代码层层嵌套,缩进越来越深,可读性极差,错误处理也变得非常繁琐,每次都需要检查 err 参数。当需要处理更复杂的逻辑,例如多个并行的异步操作,或者在不同分支中处理不同的异步链时,代码的维护成本会呈指数级增长。


二、Promises:回调地狱的救赎

为了解决回调地狱的问题,ES6 引入了 Promise。Promise 代表了一个异步操作的最终完成(或失败)及其结果值。它提供了一种更结构化、更易于管理的方式来处理异步操作。

一个 Promise 有三种状态:

  1. Pending (待定):初始状态,既没有成功,也没有失败。
  2. Fulfilled (已成功):操作成功完成。
  3. Rejected (已失败):操作失败。

Promise 的核心在于链式调用,通过 .then() 方法可以注册在 Promise 成功时执行的回调,通过 .catch() 方法可以注册在 Promise 失败时执行的回调。

让我们将之前的回调地狱示例改写成 Promise 版本:

// 模拟异步 API 调用,返回 Promise
function getUserP(userId) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log(`Getting user for ID: ${userId}`);
            // 模拟成功或失败
            if (userId === 0) {
                reject(new Error('User ID cannot be 0'));
            } else {
                const user = { id: userId, name: 'Alice', orderIds: ['order_1', 'order_2'] };
                resolve(user);
            }
        }, 500);
    });
}

function getOrdersP(orderIds) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log(`Getting orders for IDs: ${orderIds.join(', ')}`);
            // 模拟成功或失败
            if (orderIds.length === 0) {
                reject(new Error('No order IDs provided'));
            } else {
                const orders = orderIds.map(id => ({ id: id, productId: `product_${id.split('_')[1]}` }));
                resolve(orders);
            }
        }, 700);
    });
}

function getProductP(productId) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log(`Getting product for ID: ${productId}`);
            // 模拟成功或失败
            if (!productId) {
                reject(new Error('Product ID cannot be empty'));
            } else {
                const product = { id: productId, name: `Product ${productId.split('_')[1]}`, price: 100 };
                resolve(product);
            }
        }, 300);
    });
}

// 实际业务逻辑 (Promise 版)
function fetchUserDataAndProducts_PromiseChain(userId) {
    console.log(`--- Starting Promise Chain for user ${userId} ---`);
    return getUserP(userId)
        .then(user => {
            console.log('User:', user.name);
            return getOrdersP(user.orderIds); // 返回一个新的 Promise,进行链式调用
        })
        .then(orders => {
            console.log('Orders found:', orders.length);
            if (orders.length > 0) {
                const firstOrderProductId = orders[0].productId;
                return getProductP(firstOrderProductId); // 返回一个新的 Promise
            } else {
                console.log('No orders found.');
                return Promise.resolve(null); // 返回一个 resolved 的 Promise,避免链中断
            }
        })
        .then(product => {
            if (product) {
                console.log('First order product:', product.name, product.price);
            }
            console.log('All data fetched using Promises.');
        })
        .catch(error => { // 统一的错误处理
            console.error('An error occurred in Promise chain:', error.message);
        })
        .finally(() => { // 无论成功失败都会执行
            console.log('--- Promise Chain Finished ---');
        });
}

fetchUserDataAndProducts_PromiseChain(123);
fetchUserDataAndProducts_PromiseChain(0); // 模拟错误

通过 Promise,我们摆脱了深层嵌套,代码变得更加扁平,错误处理也更加集中。then 方法返回一个新的 Promise,允许我们创建清晰的异步操作链。这无疑是异步编程的一大进步。

尽管 Promise 解决了回调地狱,但在某些情况下,尤其当异步操作逻辑复杂、相互依赖性强,或者需要将异步结果传递给后续多个步骤时,Promise 链仍然可能显得冗长,可读性不如同步代码。比如,在链中需要多次从 Promise 结果中解构数据,或者在不同分支中处理不同的 Promise 链,仍然会引入一些样板代码。


三、Generators:可暂停的函数

在深入 async/await 之前,我们必须理解 JavaScript 中的 Generator 函数。Generator 是 ES6 引入的一种特殊函数,它允许函数在执行过程中暂停,并在稍后从暂停的地方恢复执行。这种“暂停与恢复”的能力是实现协程和 async/await 的基石。

Generator 函数的特点:

  1. 声明方式:使用 function* 关键字声明。
  2. yield 关键字:用于暂停 Generator 函数的执行,并返回一个值。
  3. 返回迭代器:调用 Generator 函数并不会立即执行其内部代码,而是返回一个迭代器(Iterator)对象。
  4. next() 方法:通过调用迭代器对象的 next() 方法来恢复 Generator 函数的执行,直到遇到下一个 yield 表达式或函数结束。next() 方法返回一个对象 { value: any, done: boolean }valueyield 表达式的值,done 表示 Generator 是否已执行完毕。
  5. yield 的双向通信yield 不仅可以“产出”值,还可以“接收”值。next() 方法的参数会作为上一个 yield 表达式的返回值。

让我们看一个简单的 Generator 示例:

function* simpleGenerator() {
    console.log('Generator started');
    let a = yield 'First pause'; // 暂停并返回 'First pause'
    console.log('Resumed after first yield, received:', a);
    let b = yield 'Second pause'; // 暂停并返回 'Second pause'
    console.log('Resumed after second yield, received:', b);
    return 'Generator finished'; // 函数结束,返回最终值
}

console.log('--- Starting Generator Example ---');

const gen = simpleGenerator(); // 调用 Generator 函数,返回一个迭代器对象

console.log('First next call:', gen.next()); // 启动 Generator,执行到第一个 yield
// Output:
// Generator started
// First next call: { value: 'First pause', done: false }

console.log('Second next call:', gen.next('Hello from outside!')); // 恢复执行,并将 'Hello from outside!' 传递给 a
// Output:
// Resumed after first yield, received: Hello from outside!
// Second next call: { value: 'Second pause', done: false }

console.log('Third next call:', gen.next('World!')); // 恢复执行,并将 'World!' 传递给 b
// Output:
// Resumed after second yield, received: World!
// Third next call: { value: 'Generator finished', done: true }

console.log('Fourth next call:', gen.next()); // Generator 已完成,后续调用 value 为 undefined
// Output:
// Fourth next call: { value: undefined, done: true }

console.log('--- Generator Example Finished ---');

这个例子清晰地展示了 Generator 的暂停、恢复以及双向通信能力。yield 关键字是实现这种暂停机制的关键,它使得一个函数的执行流不再是线性的、一次性的,而是可以被外部控制的。

Generator 与异步操作的联系:

思考一下,如果我们将 yield 后面跟着的不是一个简单的字符串,而是一个异步操作(例如一个 Promise),那会怎么样?
当 Generator 执行到 yield somePromise() 时,它会暂停,并把这个 Promise 返回给外部。外部代码可以等待这个 Promise 解决,然后将解决后的值通过 gen.next(resolvedValue) 传回 Generator,让它从暂停的地方继续执行。这不就是我们处理异步操作所需要的“等待”机制吗?


四、协程(Coroutine):用 Generator 驱动异步流

协程是一种用户级的、非抢占式调度机制。它允许一个函数在执行过程中暂停,并将控制权交给另一个函数,而后者可以在稍后将控制权还给前者,使其从暂停的地方继续执行。Generator 在 JavaScript 中就是一种实现协程的强大工具。

结合 Promise 和 Generator,我们可以手动构建一个协程来管理异步流程。这个思想是 async/await 背后最核心的原理。

为了更好地理解,我们来编写一个简化的“协程执行器”(或者称为“Generator Runner”),它能够自动驱动一个 Generator 函数,当 Generator 内部 yield 出一个 Promise 时,执行器会等待这个 Promise 解决,然后将结果传回 Generator 继续执行。

// 简化的协程执行器 (Generator Runner)
function run(generatorFunction) {
    const generator = generatorFunction(); // 获取 Generator 迭代器

    function step(nextFn) {
        let generatorResult;
        try {
            generatorResult = nextFn(); // 首次调用 next() 或传递上次 Promise 的结果
        } catch (e) {
            return Promise.reject(e); // Generator 内部抛出错误,作为 Promise 拒绝
        }

        const { value, done } = generatorResult;

        // 如果 Generator 已经完成,返回最终结果
        if (done) {
            return Promise.resolve(value);
        }

        // 假设 value 是一个 Promise (或者可以被 Promise.resolve 包装)
        return Promise.resolve(value).then(
            res => step(() => generator.next(res)), // Promise 成功,将结果传回 Generator 继续
            err => step(() => generator.throw(err)) // Promise 失败,将错误抛回 Generator
        );
    }

    // 启动 Generator
    return step(() => generator.next(undefined));
}

// 使用 Generator 实现异步链 (模拟 async/await)
function* fetchDataGenerator() {
    console.log('Generator: Starting fetch operations...');
    try {
        const user = yield getUserP(123); // 暂停,等待 getUserP(123) 解决
        console.log('Generator: Got user:', user.name);

        const orders = yield getOrdersP(user.orderIds); // 暂停,等待 getOrdersP 解决
        console.log('Generator: Got orders count:', orders.length);

        if (orders.length > 0) {
            const product = yield getProductP(orders[0].productId); // 暂停,等待 getProductP 解决
            console.log('Generator: Got product:', product.name);
            return product; // Generator 完成,返回最终结果
        } else {
            console.log('Generator: No orders found for user.');
            return null;
        }
    } catch (error) {
        console.error('Generator: An error occurred:', error.message);
        throw error; // 重新抛出错误,让 run 函数的 Promise 拒绝
    }
}

console.log('--- Starting Generator-based Coroutine Example ---');
run(fetchDataGenerator)
    .then(finalResult => {
        console.log('Coroutine finished successfully. Final result:', finalResult ? finalResult.name : 'N/A');
    })
    .catch(error => {
        console.error('Coroutine failed:', error.message);
    });
console.log('--- Coroutine Example Initiated ---'); // 这句话会先打印

// 模拟错误情况
function* fetchDataGeneratorWithError() {
    console.log('Generator (Error): Starting fetch operations...');
    try {
        const user = yield getUserP(0); // 这会触发 getUserP 中的 reject
        console.log('Generator (Error): Got user:', user.name); // 不会执行
    } catch (error) {
        console.error('Generator (Error): Caught error internally:', error.message);
        // 如果这里不 rethrow,外部 Promise 会 resolve
        throw error; // 重新抛出,让 run 函数的 Promise 拒绝
    }
}

console.log('n--- Starting Generator-based Coroutine Error Example ---');
run(fetchDataGeneratorWithError)
    .then(finalResult => {
        console.log('Coroutine (Error) finished successfully. Final result:', finalResult);
    })
    .catch(error => {
        console.error('Coroutine (Error) failed:', error.message);
    });
console.log('--- Coroutine Error Example Initiated ---');

在这个 run 函数中,我们看到一个完整的协程模式:

  1. 它接收一个 Generator 函数。
  2. 它通过递归地调用 step 函数来驱动 Generator。
  3. 当 Generator yield 出一个 Promise 时,step 函数会等待这个 Promise 解决。
  4. Promise 解决后,step 会将结果通过 generator.next(res) 传回 Generator,使其从暂停处继续执行。
  5. 如果 Promise 拒绝,step 会通过 generator.throw(err) 将错误抛回 Generator 内部,让 Generator 内部的 try/catch 捕获。
  6. 整个 run 函数最终返回一个 Promise,代表整个协程的最终结果。

这种模式在 async/await 出现之前,通过像 co 这样的库得到了广泛应用。co 库的作者 TJ Holowaychuk 甚至称 Generator 为“协程的未来”。它让异步代码看起来更像同步代码,解决了 Promise 链的某些复杂性。


五、async/await:语法糖的巅峰

现在,我们终于来到了 async/await。理解了 Promise、Generator 和协程模式后,async/await 的本质就呼之欲出了:它正是对上述 Generator + Promise + 协程执行器模式的语法糖!

async/await 的设计目标是让异步代码的编写和阅读体验达到与同步代码几乎一致的水平,同时保持非阻塞的特性。

async 关键字:

  • async 函数总是返回一个 Promise。如果 async 函数内部没有显式地返回 Promise,它会隐式地将返回值包裹在一个已解决的 Promise 中。
  • 如果 async 函数内部抛出错误,这个错误会被 Promise 捕获,并导致返回的 Promise 变为拒绝状态。

await 关键字:

  • await 只能在 async 函数内部使用。
  • await 后面通常跟着一个 Promise。
  • await 遇到一个 Promise 时,它会暂停 async 函数的执行,直到该 Promise 解决(fulfilled)或拒绝(rejected)。
  • 如果 Promise 解决,await 表达式的值就是 Promise 解决后的值,async 函数从暂停处继续执行。
  • 如果 Promise 拒绝,await 会抛出一个错误,这个错误可以被 async 函数内部的 try...catch 块捕获。

让我们用 async/await 重写之前的异步操作链:

// 模拟异步 API 调用,返回 Promise (同 Promise 部分的 P 函数)
// ... (getUserP, getOrdersP, getProductP 保持不变)

// 实际业务逻辑 (async/await 版)
async function fetchUserDataAndProducts_AsyncAwait(userId) {
    console.log(`--- Starting Async/Await for user ${userId} ---`);
    try {
        const user = await getUserP(userId); // 暂停,等待用户数据
        console.log('Async/Await: Got user:', user.name);

        const orders = await getOrdersP(user.orderIds); // 暂停,等待订单数据
        console.log('Async/Await: Got orders count:', orders.length);

        if (orders.length > 0) {
            const product = await getProductP(orders[0].productId); // 暂停,等待商品数据
            console.log('Async/Await: Got product:', product.name);
            return product; // async 函数返回的值会被包裹成一个 resolved Promise
        } else {
            console.log('Async/Await: No orders found for user.');
            return null;
        }
    } catch (error) {
        console.error('Async/Await: An error occurred:', error.message);
        throw error; // 抛出错误,async 函数返回的 Promise 将是 rejected 状态
    } finally {
        console.log('--- Async/Await Function Finished ---');
    }
}

// 调用 async 函数,它返回一个 Promise
fetchUserDataAndProducts_AsyncAwait(123)
    .then(finalResult => {
        console.log('Async/Await call finished successfully. Final product:', finalResult ? finalResult.name : 'N/A');
    })
    .catch(error => {
        console.error('Async/Await call failed:', error.message);
    });
console.log('--- Async/Await Example Initiated ---'); // 这句话会先打印

// 模拟错误情况
fetchUserDataAndProducts_AsyncAwait(0) // 模拟 getUserP 拒绝
    .then(finalResult => {
        console.log('Async/Await (Error) call finished successfully. Result:', finalResult);
    })
    .catch(error => {
        console.error('Async/Await (Error) call failed:', error.message);
    });

对比一下三种异步处理方式的代码:

特性 / 方法 回调函数 (Callback) Promise 链 Generator + Runner (Co-like) async/await
可读性 差 (回调地狱,深层嵌套) 较好 (扁平化链式调用) 很好 (接近同步代码,但需手动 runco 库) 极好 (几乎完全同步代码风格)
错误处理 分散,每个回调需单独处理 err 参数 集中 (.catch().then(null, rejectHandler)) 集中 (try/catch 结合 generator.throw()) 集中 (try/catch 语法)
控制流 难以管理,易出错 结构化,通过链式调用管理顺序 可暂停/恢复,通过外部执行器驱动 可暂停/恢复,由运行时隐式驱动
返回值 通过回调参数传递 返回 Promise,Promise 的解决值 run 函数返回 Promise,Generator 的 return 返回 Promise,async 函数的返回值被包裹为 Promise 解决值
实现复杂性 简单直接,但业务复杂后极难维护 需理解 Promise 概念及链式调用规则 需理解 Generator、Promise 及协程驱动逻辑 语法简单,但底层依赖 Promise 和 Generator
调试体验 堆栈信息不连贯,难以追踪异步调用链 堆栈信息相对改善,但仍有异步边界 堆栈信息相对改善 堆栈信息最接近同步代码,易于追踪

async/await 的最大优势在于其可读性可维护性。它让异步代码看起来就像同步代码一样,使得开发者能够以更直观的方式思考和组织异步逻辑。在处理连续依赖的异步操作时,async/await 极大地简化了代码。


六、深入剖析:async/await 的转换机制

现在,我们来揭示 async/await 作为语法糖的真正秘密。现代 JavaScript 引擎(如 V8)或转译工具(如 Babel)在处理 async/await 时,会将其转换为基于 Generator 和 Promise 的代码。

核心思想:
一个 async 函数在概念上会被转化为一个 Generator 函数。这个 Generator 函数会在每次 await 一个 Promise 时 yield 出那个 Promise。然后,一个隐藏的“异步协程执行器”会负责消费这个 Generator,等待 yield 出来的 Promise 解决,并将结果传回 Generator 继续执行,直到 Generator 完成。

让我们来模拟一个简单的 async 函数是如何被转换为 Generator + Runner 的。

// 原始的 async 函数
async function myAsyncFunction() {
    console.log('myAsyncFunction started');
    const result1 = await Promise.resolve(10); // 第一个 await
    console.log('After first await:', result1);
    const result2 = await new Promise(resolve => setTimeout(() => resolve(result1 + 20), 100)); // 第二个 await
    console.log('After second await:', result2);
    return result2 * 2;
}

// ----------------------------------------------------
// 概念上,myAsyncFunction 大致会被转换为如下 Generator 和 Runner 组合:
// (这只是一个简化和概念性的转换,实际引擎实现会更复杂和优化)
// ----------------------------------------------------

function myAsyncFunction_transpiled() {
    // 1. async 函数的体被封装成一个 Generator 函数
    const generator = (function* () {
        let result1, result2;
        try {
            console.log('myAsyncFunction_transpiled started');
            // await Promise.resolve(10)  =>  yield Promise.resolve(10)
            result1 = yield Promise.resolve(10);
            console.log('After first await:', result1);

            // await new Promise(...)  =>  yield new Promise(...)
            result2 = yield new Promise(resolve => setTimeout(() => resolve(result1 + 20), 100));
            console.log('After second await:', result2);

            return result2 * 2; // Generator 的返回值就是 async 函数的返回值
        } catch (e) {
            // 异步错误被捕获,并在 Generator 内部抛出
            throw e;
        }
    })(); // 立即调用以获取迭代器

    // 2. 隐藏的异步协程执行器 (类似之前我们手写的 run 函数)
    return new Promise((resolve, reject) => {
        function step(nextFn) {
            let generatorResult;
            try {
                // 调用 generator.next() 或 generator.throw()
                generatorResult = nextFn();
            } catch (e) {
                return reject(e); // Generator 内部抛出错误,Promise 拒绝
            }

            const { value, done } = generatorResult;

            // 如果 Generator 已经完成,resolve 最终值
            if (done) {
                return resolve(value);
            }

            // 递归地处理 Promise
            Promise.resolve(value).then(
                res => step(() => generator.next(res)), // Promise 解决,将结果传回 Generator
                err => step(() => generator.throw(err)) // Promise 拒绝,将错误抛回 Generator
            );
        }

        // 启动 Generator
        step(() => generator.next(undefined));
    });
}

console.log('--- Original async function call ---');
myAsyncFunction().then(res => console.log('Original async result:', res));

console.log('--- Transpiled (conceptual) async function call ---');
myAsyncFunction_transpiled().then(res => console.log('Transpiled async result:', res));

转换过程的详细步骤:

  1. async 函数体转换为 Generator 函数体

    • async 函数的整个逻辑被包裹在一个 Generator 函数 function* 中。
    • await 表达式被替换为 yield 表达式。await SomePromise 变成了 yield SomePromise
    • async 函数的返回值变成了 Generator 函数的 return 值。
    • async 函数中的 try...catch 块直接映射到 Generator 函数中的 try...catch
  2. 隐藏的协程执行器

    • 这个执行器是一个 Promise 包装器。它会在 async 函数被调用时立即执行。
    • 它会调用 Generator 的 next() 方法来启动 Generator。
    • 每当 Generator yield 出一个 Promise,执行器就会暂停,并等待这个 Promise 解决。
    • 当 Promise 解决时,执行器会将解决的值作为参数传给 Generator 的 next() 方法,恢复 Generator 的执行。
    • 如果 Promise 拒绝,执行器会调用 Generator 的 throw() 方法,将错误抛回 Generator 内部,使其内部的 try...catch 可以捕获。
    • 当 Generator 执行完毕(donetrue),执行器会根据 Generator 的 return 值来解决其自身的 Promise。

这种转换机制确保了 async/await 在提供同步代码般的简洁性的同时,底层依然是基于事件循环和 Promise 机制的非阻塞异步操作。它将复杂的异步流控制逻辑封装在运行时中,对开发者来说完全透明,从而极大地提高了开发效率和代码质量。


七、async/await 的并发与并行

虽然 await 关键字使得异步操作看起来是顺序执行的,但在实际应用中,我们常常需要并行执行多个不相关的异步操作以提高效率。async/await 结合 Promise.all()Promise.race() 可以很好地处理这些场景。

顺序执行 (默认行为):

async function sequentialOperations() {
    console.log('Starting sequential operations...');
    const result1 = await new Promise(resolve => setTimeout(() => resolve('Data 1'), 1000));
    const result2 = await new Promise(resolve => setTimeout(() => resolve('Data 2'), 500));
    console.log(result1, result2); // 总耗时约 1500ms
    return [result1, result2];
}
// sequentialOperations();

并发执行 (使用 Promise.all):

如果两个异步操作之间没有依赖关系,我们可以让它们同时开始执行,等待所有都完成后再继续。

async function concurrentOperations() {
    console.log('Starting concurrent operations...');
    const promise1 = new Promise(resolve => setTimeout(() => resolve('Data A'), 1000));
    const promise2 = new Promise(resolve => setTimeout(() => resolve('Data B'), 500));

    // Promise.all 等待所有 Promise 完成,并返回一个包含所有结果的数组
    const results = await Promise.all([promise1, promise2]);
    console.log(results[0], results[1]); // 总耗时约 1000ms (最长的那个 Promise 的时间)
    return results;
}
// concurrentOperations();

竞态条件 (使用 Promise.race):

当只需要一个异步操作最快完成时,可以使用 Promise.race

async function raceOperations() {
    console.log('Starting race operations...');
    const promiseFast = new Promise(resolve => setTimeout(() => resolve('Fast Data'), 200));
    const promiseSlow = new Promise(resolve => setTimeout(() => resolve('Slow Data'), 1000));

    // Promise.race 返回第一个解决或拒绝的 Promise 的结果
    const fastestResult = await Promise.race([promiseFast, promiseSlow]);
    console.log('Fastest result:', fastestResult); // 输出 'Fast Data'
    return fastestResult;
}
// raceOperations();

八、async/await 的优势与局限

优势:

  1. 极佳的可读性与可维护性:代码看起来像同步,逻辑流清晰,大大降低了理解和修改异步代码的难度。
  2. 更简单的错误处理:可以使用熟悉的 try...catch 块来捕获异步操作中的错误,与同步错误处理方式一致,避免了 Promise 链中每个 .then() 后面都可能需要 .catch() 的情况。
  3. 更好的调试体验:在 async 函数内部,当 await 暂停时,调试器仍然可以停留在 async 函数的上下文中,堆栈信息也更加直观和连贯,这比回调或 Promise 链的调试要容易得多。
  4. 条件语句和循环更自然:在 async 函数中,可以直接在 if/elsefor/while 循环中使用 await,编写复杂的异步流程控制变得非常自然。
  5. 与 Promise 的无缝集成async 函数本身返回 Promise,并且 await 可以等待任何 Promise-like 对象,这使得它能够与现有基于 Promise 的库和 API 无缝协作。

局限性(或需要注意的点):

  1. await 必须在 async 函数中使用:不能在普通函数或全局作用域直接使用 await。顶层 await (Top-level await) 虽已在 ES2022 中引入,但仍需环境支持,且主要用于模块的异步初始化。
  2. 滥用 await 可能导致性能问题:如果多个异步操作之间没有依赖关系,但却错误地使用 await 逐个等待,会导致串行执行,降低效率。应使用 Promise.all() 来实现并发。
  3. 理解 Promise 仍是基础async/await 只是 Promise 的语法糖,其底层机制和错误传播仍然依赖于 Promise。因此,对 Promise 的理解是使用 async/await 的前提。
  4. 事件循环的理解:虽然 async/await 使得代码看起来是同步的,但它本质上仍然是异步的,并且是在 JavaScript 的事件循环上运行的。深入理解事件循环对于避免一些难以察觉的异步问题仍然至关重要。

九、超越 Generator:现代引擎的优化

值得一提的是,虽然 Generator 和协程模式是理解 async/await 概念模型和其作为语法糖的关键,但现代 JavaScript 引擎在实现 async/await 时,可能并不会真的在运行时创建 Generator 对象。

为了性能优化,许多引擎可能会直接将 async 函数编译成一个状态机。这个状态机负责跟踪函数的执行进度,在每个 await 点暂停并保存当前状态,在 Promise 解决后恢复执行并跳转到下一个状态。这种状态机实现避免了 Generator 对象的额外开销,从而提供了更高的执行效率。

这进一步强调了 async/await 的“语法糖”本质:它提供了一种高级的抽象,其底层实现可以有多种方式(Generator-based 或 State Machine-based),但对外暴露的行为和编程模型保持一致。对于开发者而言,我们只需专注于其提供的简洁异步编程接口即可。


从回调地狱的挣扎,到 Promise 的结构化管理,再到 Generator 提供的可暂停执行能力,最终演变为 async/await 这种优雅的语法糖,JavaScript 异步编程的历史是一部不断追求更高抽象、更佳开发体验的演进史。async/await 结合了 Promise 的状态管理和 Generator 的控制流特性,为我们提供了一种前所未有的方式来编写和理解复杂的异步代码。它并非魔法,而是基于底层机制的精妙封装,让我们能够以同步的思维,驾驭异步的洪流。理解其本质,将有助于我们更深入地掌握 JavaScript,并编写出更健壮、更高效的应用程序。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注