JavaScript 从零基础到精通系列:异步编程与网络请求

摘要: 网页常常需要从服务器获取数据而无需刷新页面,这就需要异步操作。本篇将逐步讲解 JavaScript 的异步模型:回调函数、Promise 和 async/await。你将学会使用 Fetch API 与服务器交互,理解事件循环的基本概念,并在此基础上搭建一个实时汇率查询小应用,跨入前后端数据交互的大门。


一、同步与异步

JavaScript 是单线程语言,一个时间只能做一件事。如果任务耗时很长(比如网络请求),同步执行会阻塞页面,导致卡顿。因此,浏览器提供了异步 API(定时器、AJAX、事件监听等),这些操作交给浏览器其他线程处理,完成后通过回调函数通知 JS 主线程。

二、回调函数与回调地狱

最基础的异步模式就是回调函数。

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>异步执行顺序</title>
</head>
<body>

<script>
console.log('========== 代码开始执行 ==========');

// 1. 同步任务:立刻执行
console.log('① 开始');

// 2. 异步任务:放入任务队列,等待 1秒 后执行
setTimeout(() => {
    console.log('③ 1 秒后执行(异步任务)');
}, 1000);

// 3. 同步任务:立刻执行
console.log('② 结束');

console.log('========== 同步代码执行完毕 ==========');
</script>

</body>
</html>

当多个异步任务需要顺序执行时,会出现"回调地狱",代码层层嵌套,难以维护:

javascript 复制代码
// 模拟地狱
getUser(userId, function(user) {
    getPosts(user.id, function(posts) {
        getComments(posts[0].id, function(comments) {
            // ...
        });
    });
});

三、Promise:优雅的异步方案

ES6 引入的 Promise 对象,代表一个异步操作的最终完成或失败。

基本创建和消费

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>Promise 完整示例</title>
</head>
<body>

<script>
// 1. 创建 Promise 对象(封装异步任务)
const promise = new Promise((resolve, reject) => {
    console.log("1. 开始执行异步操作...");

    // 模拟异步请求(定时器)
    setTimeout(() => {
        const success = true; 
        // 你可以改成 false 测试失败情况
        
        if (success) {
            // ✅ 成功:调用 resolve,把结果传给 .then()
            resolve("✅ 数据获取成功");
        } else {
            // ❌ 失败:调用 reject,把错误传给 .catch()
            reject("❌ 出错了:网络请求失败");
        }
    }, 1500);
});

// 2. 使用 Promise
promise
    .then((result) => {
        console.log("2. then 收到:", result);
        return "✅ 下一步处理数据"; // 可以继续传递给下一个 then
    })
    .then((nextResult) => {
        console.log("3. 第二个 then 收到:", nextResult);
    })
    .catch((error) => {
        // ❌ 捕获所有错误
        console.error("❌ 捕获异常:", error);
    })
    .finally(() => {
        // 🎯 无论成功/失败 都会执行
        console.log("🎯 finally:无论成败,我都会执行!");
    });
</script>

</body>
</html>

Promise 的链式调用 完美解决了回调地狱,错误可以被最尾端的 catch 捕获。

常用静态方法

  • Promise.resolve(value) / Promise.reject(reason)

  • Promise.all([p1, p2, ...]):所有 Promise 都成功才成功,返回结果数组,一个失败整体失败。

  • Promise.allSettled([p1, p2]):等所有 Promise 敲定,不管成功失败,返回状态数组(ES2020)。

  • Promise.race([p1, p2]):返回第一个敲定的 Promise 的结果。

四、async/await:让异步代码像同步

ES2017 引入的 async/await 是 Promise 的语法糖,使得异步代码写起来像同步,可读性大大提高。

javascript 复制代码
async function fetchData() {
    try {
        // 发送网络请求
        const response = await fetch('https://api.example.com/data');
        
        // 如果网络响应失败(404/500),手动抛出错误
        if (!response.ok) throw new Error('网络响应失败');
        
        // 等待解析 JSON
        const data = await response.json();
        
        // 打印并返回数据
        console.log(data);
        return data;

    } catch (error) {
        // 捕获所有错误
        console.error('请求失败:', error);
    }
}

规则:

  • async 函数自动返回一个 Promise。

  • await 必须在 async 函数内使用,它会暂停函数执行,等待 Promise 完成。

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>async/await 数据请求</title>
</head>
<body>
    <h3>请求结果:</h3>
    <pre id="result"></pre>

<script>
// 完善版 async/await 请求函数
async function fetchData() {
    const resultDom = document.getElementById('result');
    
    try {
        // 显示加载中
        resultDom.textContent = "加载中...";

        // 1. 发送请求(使用真实公开接口)
        const response = await fetch('https://jsonplaceholder.typicode.com/todos/1');
        
        // 2. 判断网络响应是否成功
        if (!response.ok) {
            throw new Error(`请求错误:${response.status}`);
        }

        // 3. 解析 JSON 数据
        const data = await response.json();
        console.log('✅ 获取成功:', data);

        // 4. 显示到页面
        resultDom.textContent = JSON.stringify(data, null, 2);
        return data;

    } catch (error) {
        // 统一捕获所有错误:网络错误、逻辑错误、解析错误
        console.error('❌ 请求失败:', error.message);
        resultDom.textContent = '请求失败:' + error.message;
        return null;
    }
}

// 执行请求
fetchData();
</script>
</body>
</html>

五、Fetch API:现代网络请求

fetch() 是浏览器内置的、基于 Promise 的 API,取代了老旧的 XMLHttpRequest。

GET 请求

javascript 复制代码
fetch('https://api.github.com/users/octocat')
    .then(res => res.json())
    .then(data => console.log(data))
    .catch(err => console.error(err));

POST 请求

javascript 复制代码
fetch('https://jsonplaceholder.typicode.com/posts', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json'
    },
    body: JSON.stringify({
        title: 'foo',
        body: 'bar',
        userId: 1
    })
})
.then(res => res.json())
.then(data => console.log('创建成功:', data));

处理响应response.ok 判断状态码是否在 200-299。response.json() 解析 JSON,此外还有 .text().blob() 等。

六、事件循环 (Event Loop) 宏观理解

了解事件循环对写出高效的异步代码很有帮助。简单模型:

调用栈(Call Stack)执行同步代码。

遇到异步 API(如 setTimeout、fetch),交给浏览器其他线程处理,处理完后回调放入任务队列(宏任务与微任务)。

当调用栈清空时,事件循环先清空微任务队列(Promise.then/catch、MutationObserver),再取出一个宏任务(setTimeout、setInterval、I/O)执行,循环往复。

javascript 复制代码
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
// 输出:1 4 3 2

七、实战:实时汇率转换器

我们将使用免费汇率 API (exchangerate-api.com 示例) 来构建一个货币转换器。为了安全,API key 应放在后端,这里示例仅作学习。

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>汇率转换器</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
            font-family: Arial, sans-serif;
        }

        body {
            display: flex;
            justify-content: center;
            align-items: center;
            min-height: 100vh;
            background: #f5f7fa;
        }

        .converter {
            background: white;
            padding: 30px;
            border-radius: 12px;
            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
            width: 400px;
            text-align: center;
        }

        input, select, button {
            width: 100%;
            padding: 10px;
            margin: 8px 0;
            border-radius: 6px;
            border: 1px solid #ddd;
            font-size: 16px;
        }

        button {
            background: #007bff;
            color: white;
            border: none;
            cursor: pointer;
        }

        button:hover {
            background: #0056b3;
        }

        #result {
            margin-top: 15px;
            font-size: 18px;
            font-weight: bold;
            color: #333;
        }

        .tip {
            font-size: 12px;
            color: #666;
            margin-top: 10px;
        }
    </style>
</head>

<body>
    <div class="converter">
        <h2>汇率转换器</h2>
        <!-- 输入要转换的金额 -->
        <input type="number" id="amount" placeholder="请输入金额" value="1">
        <!-- 原始货币下拉框 -->
        <select id="fromCurrency"></select>
        <span>→</span>
        <!-- 目标货币下拉框 -->
        <select id="toCurrency"></select>
        <!-- 转换按钮 -->
        <button id="convertBtn">立即转换</button>
        <!-- 结果显示区域 -->
        <p id="result"></p>
        <div class="tip">实时汇率来源:exchangerate-api.com</div>
    </div>

    <script>
        // 🔸 API 地址:获取美元为基准的所有货币汇率
        const API_URL = 'https://open.er-api.com/v6/latest/USD';

        // 🔸 用来存储所有货币的汇率对象(全局方便调用)
        let rates = {};

        // ==============================================
        // 异步函数:从 API 获取最新汇率
        // ==============================================
        async function fetchRates() {
            // 获取结果显示元素
            const resultEl = document.getElementById('result');
            
            // 页面提示:正在加载
            resultEl.textContent = '加载汇率中...';

            try {
                // 1. 发送网络请求获取汇率数据
                const response = await fetch(API_URL);

                // 2. 判断请求是否成功(状态码 200-299)
                if (!response.ok) throw new Error('获取汇率失败');

                // 3. 将返回的数据解析为 JSON 格式
                const data = await response.json();

                // 4. 将汇率数据存入全局变量
                rates = data.rates;

                // 5. 把货币代码填充到下拉选择框
                populateSelectors(Object.keys(rates));

                // 6. 加载完成提示
                resultEl.textContent = '加载完成,请开始转换';
            } catch (err) {
                // 捕获错误:网络失败、接口异常等
                resultEl.textContent = '⚠️ 加载汇率失败';
                console.error('错误信息:', err);
            }
        }

        // ==============================================
        // 函数:将货币代码填充到两个下拉框
        // currencies:货币代码数组,如 ['USD','CNY','EUR']
        // ==============================================
        function populateSelectors(currencies) {
            // 获取两个下拉框元素
            const fromSelect = document.getElementById('fromCurrency');
            const toSelect = document.getElementById('toCurrency');

            // 循环所有货币代码,添加到下拉选项
            currencies.forEach(code => {
                // 创建选项:new Option(显示文字, value值)
                fromSelect.add(new Option(code, code));
                toSelect.add(new Option(code, code));
            });

            // 🔸 设置默认选中值:美元 → 人民币
            fromSelect.value = 'USD';
            toSelect.value = 'CNY';
        }

        // ==============================================
        // 点击转换按钮执行逻辑
        // ==============================================
        document.getElementById('convertBtn').addEventListener('click', () => {
            // 1. 获取输入框金额
            const amountInput = document.getElementById('amount');
            const amount = parseFloat(amountInput.value);

            // 2. 获取选中的原始货币 和 目标货币
            const from = document.getElementById('fromCurrency').value;
            const to = document.getElementById('toCurrency').value;

            // --------------------------
            // 🔸 校验输入是否合法
            // --------------------------
            // 如果不是数字 或 金额 ≤ 0,提示错误
            if (isNaN(amount) || amount <= 0) {
                alert('请输入有效的金额!');
                amountInput.focus(); // 让输入框重新聚焦
                return; // 停止执行
            }

            // 如果汇率还没加载完成,不能转换
            if (!rates[from] || !rates[to]) {
                alert('货币汇率未加载完成,请稍候');
                return;
            }

            // --------------------------
            // 🔸 核心汇率计算公式
            // --------------------------
            // 公式:目标金额 = 输入金额 / 原始货币汇率 * 目标货币汇率
            const result = (amount / rates[from]) * rates[to];

            // --------------------------
            // 显示结果(保留 2 位小数)
            // --------------------------
            document.getElementById('result').textContent =
                `${amount} ${from} = ${result.toFixed(2)} ${to}`;
        });

        // ==============================================
        // 页面一加载就自动获取汇率
        // ==============================================
        fetchRates();
    </script>
</body>
</html>

这个项目完美融合了 async/awaitfetch、DOM 操作和事件监听,体现了真实项目的开发流程。


总结: 我们从回调函数讲到 Promise 再到 async/await,这是现代 JavaScript 异步编程的主线。掌握了 Fetch API,你就打开了与服务器通信的大门。事件循环的知识帮助你写出更可靠、更高效的代码。此刻,你已具备了前后端交互的核心技能。接下来,我们将进行最后的拼图:面向对象、模块化,并运用全套知识打造一个大型项目------我的任务管家。


如果这篇文章帮你解决了实操上的困惑,别忘记点击点赞、分享 ,也可以留言告诉我你遇到的其它问题,我会尽快回复。动手练习是掌握编程最快的方法,请务必亲手敲一遍本文的所有示例代码,并截图保存你的成果。你的关注是我坚持原创和细节共享的力量来源,谢谢大家。

相关推荐
卡卡军1 小时前
🌈 react-sketch-ruler v3 升级之旅:当 React 遇上跨框架标尺引擎
前端·react.js
Asmewill1 小时前
DeepAgents学习笔记三(Backend记忆存储)
前端
Alan Lu Pop2 小时前
前端开发助手
前端·智能体
程序员鱼皮2 小时前
我用 GitHub 仓库养 AI 龙虾,自动开发上线项目!保姆级教程
前端·人工智能·ai·程序员·github·编程·ai编程
27669582922 小时前
京东随机变速滑块拼图验证码识别(京东E卡)
java·服务器·前端·python·京东滑块·京东变速滑块·京东e卡绑卡
এ慕ོ冬℘゜2 小时前
手写生产级 jQuery Toast 轻量提示组件|零插件依赖、动画流畅、极简高
前端·javascript·jquery
Oneslide3 小时前
UI设计-企业OA风格
前端
程序员海军3 小时前
我用了 8 个月 Codex CLI,总结出这套 AI 编程工作流
前端·后端·aigc
大家的林语冰3 小时前
Express 团队官宣:全新网站正式上线,Logo 重做,支持两个主版本文档无缝切换!
javascript·node.js·express