深入浅出 JavaScript 异步编程:如何优雅地等待 API 请求返回

在构建现代 Web 应用程序时,与服务器进行异步通信是我们几乎每天都要面对的任务。你是否曾遇到过这样的情况:你发起了一个 API 请求去获取用户数据,但在数据返回之前,你的代码已经执行到了下一行,结果导致程序崩溃或显示 undefined?这正是 JavaScript 单线程异步特性带来的“双刃剑”。

在这篇文章中,我们将深入探讨 JavaScript 的异步机制,并通过实战代码演示如何从基础的回调处理过渡到现代的 Async/Await 模式。我们将一起搭建一个本地测试环境,通过对比“不等待”和“正确等待”两种场景,让你彻底掌握如何让 JavaScript 乖乖地等待 API 请求返回。无论你是初级开发者还是希望巩固异步编程概念的老手,这篇指南都将为你提供实用的见解和最佳实践。

环境准备:搭建本地模拟 API

为了演示网络请求的真实行为,我们需要一个可以交互的服务器。虽然你可以直接调用公共 API,但在本地搭建一个简单的 Express 服务器能让我们更清楚地看到请求和响应的每一个细节。我们将创建一个基于 Node.js 的简单 API,它将模拟网络延迟并返回 JSON 数据。

首先,请确保你的环境中安装了 Node.js。接下来,我们需要初始化项目并安装必要的依赖包——INLINECODEb5fb0f6e(用于构建服务器)和 INLINECODE013b1f5e(用于处理跨域请求,防止我们在本地测试时遇到浏览器安全限制)。

请在终端中运行以下命令:

# 初始化项目(如果还没有 package.json)
npm init -y

# 安装 Express 和 CORS
npm install express cors axios

> 💡 实用见解:为什么要安装 INLINECODE863a7f47?虽然 Node.js 原生的 INLINECODE4656fa8e 现在已经非常普及,但 INLINECODE6760cea9 在处理 JSON 数据转换、请求拦截和错误处理方面依然具有强大的优势,且在许多遗留项目中广泛使用。在本文的客户端示例中,我们将使用 INLINECODEf7a7473e 来发起请求。

现在,让我们创建一个名为 server.js 的文件。这个文件将包含我们后端 API 的所有逻辑。我们将创建两个端点:一个用于发送简单的欢迎消息,另一个用于模拟数据获取。

#### 创建 server.js 文件

// 引入必要的模块
const express = require(‘express‘);
const cors = require(‘cors‘);

// 创建 Express 应用实例
const app = express();
const PORT = 5000;

// 使用 CORS 中间件,允许跨域请求
// 这对于前端(运行在不同端口)访问后端至关重要
app.use(cors());

// 定义 API 端点:模拟获取数据
// 当客户端访问 /getData 时,这个函数将被执行
app.get(‘/getData‘, (req, res) => {
  // 模拟一个 JSON 响应
  const responseData = {
    status: ‘success‘,
    message: ‘这是来自 Express API 的响应数据‘,
    timestamp: new Date().toISOString()
  };
  
  // 将数据作为 JSON 格式发送回客户端
  res.json(responseData);
});

// 根路径端点,用于确认服务器正在运行
app.get(‘/‘, (req, res) => {
  res.send(‘服务器运行正常!欢迎来到 API 测试环境。‘);
});

// 启动服务器并监听指定端口
app.listen(PORT, () => {
  console.log(`本地 API 服务器已启动: http://127.0.0.1:${PORT}`);
});

要运行此服务器,请在终端执行:

node server.js

现在,我们有了一个运行在 http://127.0.0.1:5000 的后台服务。接下来,让我们编写客户端代码来与它交互。

问题现场:不使用 Async/Await 的混乱

在我们学会“等待”之前,先让我们看看“不等待”会发生什么。这是初学者最容易陷入的陷阱:试图从异步调用中直接返回结果。

JavaScript 是单线程的,但它依赖于事件循环来处理异步操作。当你发起一个网络请求时,JavaScript 不会停下来等待服务器响应,而是会把这个请求交给浏览器后台处理,然后立即继续执行下一行代码。这就是为什么我们称这种行为为“非阻塞”。

让我们来看一个反面教材,演示如果不处理异步逻辑,代码执行顺序会变得多么混乱。

#### 示例 1:错误的同步思维(不使用 Async/Await)

// 引入 axios 用于发起 HTTP 请求
const axios = require(‘axios‘);

// 这是一个试图“获取”数据的函数,但它并没有真正等待
function makeGetRequest(path) {
    // 注意:这里虽然发起了请求,但没有 return 语句返回 Promise
    // 或者说,下面的 .then() 是回调,但函数本身立刻返回了 undefined
    axios.get(path).then(
        (response) => {
            var result = response.data;
            console.log(‘1. [API 内部] 数据已处理:‘, result.message);
            return (result); // 这个 return 只是为了 .then() 链式调用,不会返回给外部的 main 函数
        },
        (error) => {
            console.log(‘错误:‘, error);
        }
    );
    // 🚨 关键点:由于 axios 是异步的,函数在请求完成之前就会执行到这里
    // 由于没有显式 return,这里默认返回 undefined
}

function main() {
    console.log(‘--- 开始执行(不使用 Async/Await) ---‘);
    
    // 我们希望这里能拿到 API 的数据
    let response = makeGetRequest(‘http://127.0.0.1:5000/getData‘);
    
    console.log(‘2. [Main] 此时拿到的 response 是:‘, response);
    console.log(‘3. [Main] Statement 2: 这行代码会在 API 返回之前执行吗?‘);
}

main();

#### 执行结果分析

如果你运行上面的代码,你会看到类似这样的输出顺序:

--- 开始执行(不使用 Async/Await) ---
2. [Main] 此时拿到的 response 是: undefined
3. [Main] Statement 2: 这行代码会在 API 返回之前执行吗?
1. [API 内部] 数据已处理: ...

看到了吗?INLINECODE8bb9b636 和 INLINECODEf7710810 的日志出现在 API 响应之前,而且 INLINECODE7874346d 的值是 INLINECODEbb383b3f。这是因为 INLINECODE76be9d4e 函数没有等待 INLINECODE1fb795da 完成,它只是发起了请求就立刻结束了。在真实的业务逻辑中,这会导致严重的逻辑错误,比如试图渲染一个还没加载好的用户头像,导致程序报错。

解决方案:Async/Await 的魔法

为了解决上述问题,我们需要告诉 JavaScript:“暂停这个函数的执行,直到这个 Promise 解决为止”。这正是 INLINECODEeba20a55 和 INLINECODEc8bbc3c9 关键字的用途。

  • INLINECODEe0413d1b: 将一个函数声明为异步函数。它自动允许我们在函数内部使用 INLINECODEad3aec2b,并且隐式地将函数的返回值包装在一个 Promise 中。
  • await: 放置在返回 Promise 的调用之前。它会暂停函数的执行,直到 Promise 完成(resolve 或 reject),并返回 Promise 的结果。

让我们重构之前的代码,展示如何优雅地解决这个问题。

#### 示例 2:正确使用 Async/Await

在这个例子中,我们需要稍微调整一下思路。为了配合 INLINECODE1ff4638c,我们需要确保调用的函数返回一个 Promise(INLINECODEbdee1db3 已经做到了这一点,但在原文章的第二个示例中,作者手动包装了一个 Promise,这是一种为了演示底层原理的做法。为了代码的现代化和简洁,我将展示两种方式:直接使用 axios 的 Promise,以及手动包装 Promise 的方式以匹配原文章的结构)。

方式 A:直接使用 Axios(推荐,更现代)

const axios = require(‘axios‘);

// 直接使用 async/await 处理 axios 请求
async function fetchDataModern() {
    try {
        console.log(‘1. 发起请求,开始等待...‘);
        // await 会暂停这里,直到请求完成
        const response = await axios.get(‘http://127.0.0.1:5000/getData‘);
        
        // 只有请求完成后,才会执行这一行
        console.log(‘2. 数据返回成功:‘, response.data.message);
        return response.data;
    } catch (error) {
        console.error(‘请求出错:‘, error);
    }
}

async function mainModern() {
    console.log(‘--- 现代方式执行 ---‘);
    let result = await fetchDataModern();
    console.log(‘3. Statement 2: 现在我们确保这行代码在数据返回之后才执行。‘);
}

mainModern();

方式 B:手动包装 Promise(深入理解原理)

为了让你更清楚地理解 Promise 的工作原理(这也是原文章试图展示的重点),让我们手动创建一个返回 Promise 的函数,然后使用 await 去等待它。

const axios = require(‘axios‘);

// 这个函数显式地返回一个 Promise
function makeGetRequest(path) {
    return new Promise(function (resolve, reject) {
        // 使用 axios 发起请求
        axios.get(path).then(
            (response) => {
                var result = response.data;
                console.log(‘1. [内部 Promise] 请求处理中...‘);
                // 成功时调用 resolve,将数据传出去
                resolve(result);
            },
            (error) => {
                // 失败时调用 reject
                reject(error);
            }
        );
    });
}

// 使用 async 关键字定义主函数
async function main() {
    try {
        console.log(‘--- 开始执行(使用 Async/Await) ---‘);
        
        // ⚡ 关键点:await 关键字暂停了 main 函数的执行
        // 直到 makeGetRequest 返回的 Promise 被 resolve
        let result = await makeGetRequest(‘http://127.0.0.1:5000/getData‘);
        
        // 这行代码只有等 await 结束后才会执行
        console.log(‘2. 结果数据:‘, result);
        
        console.log(‘3. Statement 2: 此时我们已经安全地拿到了数据。‘);
        
    } catch (error) {
        console.error(‘捕获到异常:‘, error);
    }
}

main();

#### 执行结果分析

现在,无论你运行方式 A 还是方式 B,控制台的输出顺序都是符合直觉的:

--- 开始执行(使用 Async/Await) ---
1. [内部 Promise] 请求处理中...
2. 结果数据: { ... }
3. Statement 2: 此时我们已经安全地拿到了数据。

通过 INLINECODE605af9ab,我们成功地将异步代码“变”得像同步代码一样易读和维护。程序严格按顺序执行,没有意外的 INLINECODEbd59a9c2,也没有乱序的日志。

进阶见解:最佳实践与常见陷阱

掌握了基本用法后,让我们来谈谈一些在实际开发中非常重要的高级技巧。

#### 1. 错误处理:不要忘记 Try/Catch

当你使用 INLINECODE73b06e07 时,Promise 可能会被 INLINECODE3a2c253b。如果不用 try...catch 包裹,错误可能会在顶层冒泡,导致 Node.js 进程崩溃。在浏览器中,虽然不会崩溃,但代码会停止执行。

#### 2. 并发请求:Promise.all

如果你需要同时发起多个独立的 API 请求(例如获取用户信息和获取用户订单),不要写成串行的 await,那样会非常慢。

❌ 慢速做法(串行):

const user = await axios.get(‘/user/1‘); // 等待 1s
const orders = await axios.get(‘/orders/1‘); // 等待 1s
// 总耗时:2s

✅ 快速做法(并行):

const [user, orders] = await Promise.all([
    axios.get(‘/user/1‘),
    axios.get(‘/orders/1‘)
]);
// 总耗时:1s

#### 3. 处理循环中的异步请求

如果你想遍历一个数组并为每个元素发起请求,INLINECODEce9b9d0d 循环通常不兼容 INLINECODE32f0d4cc。请改用 for...of 循环。

const ids = [1, 2, 3];

// ⚠️ forEach 中的 await 可能不会按预期工作
// ids.forEach(async (id) => {
//     await fetchData(id);
// });

// ✅ 推荐使用 for...of
for (const id of ids) {
    const data = await fetchData(id);
    console.log(data);
}

总结

在这篇文章中,我们通过搭建 Express 服务器和编写对比示例,深入了解了 JavaScript 处理异步请求的演变过程。从最初的回调地狱到后来混乱的链式调用,再到如今优雅的 Async/Await,JavaScript 的异步编程变得越来越人性化。

关键要点:

  • JavaScript 默认是异步的,它会立即执行后续代码而不等待网络请求。
  • async 函数返回一个 Promise。
  • INLINECODE9f88ce28 会暂停 INLINECODEc8c996d8 函数的执行,直到 Promise 解决。
  • 始终使用 INLINECODE6d95bf66 块来处理 INLINECODE23eb13bd 可能产生的错误。
  • 利用 Promise.all 来并行处理独立的请求,优化性能。

希望这篇指南能帮助你彻底理解如何在 JavaScript 中等待 API 请求。现在,你可以自信地重构那些混乱的异步代码了!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。如需转载,请注明文章出处豆丁博客和来源网址。https://shluqu.cn/52528.html
点赞
0.00 平均评分 (0% 分数) - 0