深入理解 JavaScript 异步编程:从回调到 async/await

## 手撕:sleep,Promise.all,红绿灯

JavaScript 是单线程的,这意味着它一次只能执行一个任务。然而,在处理耗时操作(如网络请求、定时器)时,我们不能让程序停下来等待。因此,异步编程应运而生。

本文将带你回顾 JavaScript 异步编程的几个重要阶段,并探讨它们的优缺点,帮你构建一个清晰、系统的知识框架。

1. 回调函数:异步编程的起点

最早期的异步编程主要依赖回调函数(Callback)。当一个异步操作完成后,它会调用你提供的回调函数来处理结果。

// 示例:使用回调函数实现异步
function red() {
    setTimeout(() => {
        console.log('red');
        // 继续调用下一个异步任务
        green();
    }, 3000);
}

function green() {
    setTimeout(() => {
        console.log('green');
        yellow();
    }, 2000);
}

function yellow() {
    setTimeout(() => {
        console.log('yellow');
    }, 1000);
}

red();

优点: 简单直观,容易理解。

缺点: - 回调地狱(Callback Hell): 当多个异步操作需要按顺序嵌套执行时,代码会变得层层缩进,难以阅读和维护。

  • 错误处理分散: 错误处理逻辑需要分散在每个回调函数中,不够集中。

2. Promise:链式调用的救星

为了解决回调地狱问题,ES6 引入了 Promise。它提供了一种更优雅、可预测的方式来处理异步操作。Promise 代表一个异步操作的最终结果,它有三种状态:pending(进行中)、fulfilled(已成功)和 rejected(已失败)。

Promise A+规范是什么

第一点:Promise 的状态

它规定了 Promise 必须有且只能有三种状态:pending(进行中)、**fulfilled(已成功)**和 rejected(已失败)

状态的转换是单向且不可逆的。

第二点:then 方法

then 方法是整个规范的核心。它规定了 then 方法必须返回一个新的 Promise。正是因为这个特性,我们才能进行链式调用。

它接收两个可选的回调函数:onFulfilled(成功时调用)和 onRejected(失败时调用)。这两个回调函数必须是异步执行的,保证了 Promise 异步非阻塞的特性。

第三点:Promise 的解决过程

  • 如果你的回调函数返回一个普通值,那么下一个 Promise 会立即成功,并以这个值为结果。
  • 如果你的回调函数返回一个新的 Promise,那么下一个 Promise 的状态和结果,将完全取决于这个新 Promise。这就像是“嵌套”了一层异步,整个链条依然能平稳运行。
  • 如果你的回调函数抛出了一个错误,那么下一个 Promise 就会被立即拒绝。

使用 .then().catch() 方法,我们可以轻松地将异步任务串联起来。

// 示例:使用 Promise 链式调用
const sleep = (fn, time) => new Promise(resolve => {
    setTimeout(() => {
        fn();
        resolve();
    }, time);
});

sleep(() => red(), 3000)
    .then(() => sleep(() => green(), 2000))
    .then(() => sleep(() => yellow(), 1000))
    .catch(e => console.error(e));

优点:

  • 链式调用: .then() 允许我们将异步任务以链式方式组织,代码更加扁平化。
  • 集中错误处理: 只需要一个 .catch() 就可以捕获整个 Promise 链中的错误。

3. async/await:异步编程的终极优雅

ES2017 引入的 async/await 是在 Promise 基础上的一层语法糖。它让异步代码的写法看起来就像同步代码一样,极大地提高了可读性。

async 函数会返回一个 Promise,而 await 关键字会暂停 async 函数的执行,直到其后的 Promise 成功解决。

// 示例:使用 async/await
async function runTrafficLights() {
    try {
        await sleep(() => red(), 3000);
        await sleep(() => green(), 2000);
        await sleep(() => yellow(), 1000);
        console.log('所有任务成功完成!');
    } catch (error) {
        console.error('任务执行失败:', error.message);
    }
}

runTrafficLights();

优点:

  • 极高的可读性: 代码逻辑清晰,几乎和同步代码无异。
  • 强大的错误处理: 可以直接使用 try...catch 块来捕获异步操作的错误。

4. 异步循环的陷阱:forEachawait

这是一个常见的面试考点。尽管 forEach 是一个遍历数组的常用方法,但它和 await 的组合方式并不能实现串行执行。

为什么 forEach 无效? forEach 的回调函数本身是同步执行的,它会立即遍历完数组中的所有元素,并为每个元素开启一个独立的异步任务。因此,所有任务会并发执行,而不是按顺序等待。

// ❌ 错误示范:并发执行
urls.forEach(async (url) => {
    // 这里不会等待,所有任务同时开始
    const response = await fetch(url);
    console.log(response);
});

// ✅ 正确做法:串行执行
async function fetchUrlsInOrder(urls) {
    for (const url of urls) {
        // await 会暂停整个循环,直到 Promise 完成
        const response = await fetch(url);
        console.log(response);
    }
}

5. 全局错误处理

在异步编程中,如果 Promise 没有被 .catch()try...catch 捕获,就会导致未处理的拒绝(unhandled rejection)。为了防止程序崩溃,我们可以设置全局的错误监听。

// 全局 Promise 错误处理
window.addEventListener('unhandledrejection', event => {
    console.error('未处理的 Promise 拒绝:', event.reason);
    event.preventDefault(); // 防止默认错误处理
});

// 全局普通错误处理
window.addEventListener('error', event => {
    console.error('全局错误:', event.error);
});

总结

  • 回调是最早的异步解决方案,但易陷入“回调地狱”。
  • Promise 解决了回调地狱问题,提供了链式调用和集中的错误处理。
  • async/await 是基于 Promise 的语法糖,让异步代码像同步代码一样直观,是现代 JavaScript 异步编程的首选。
  • 在异步循环中,请使用 for 或 for...of 循环来确保串行执行,避免 forEach 带来的并发问题。
全部评论
考点:手撕sleep,Promise.all
点赞 回复 分享
发布于 08-20 22:17 山东
卷我
点赞 回复 分享
发布于 08-20 22:11 上海

相关推荐

小浪_Coding:这没必要搞对立 如果打算就业本科肯定是最好的, 读研更多去一些国央企,事业单位, 花3年时间读研为了进大厂其实性价比一般
点赞 评论 收藏
分享
评论
1
1
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务