JavaScript 从零基础到精通系列:异步编程与网络请求
摘要:网页常常需要从服务器获取数据而无需刷新页面,这就需要异步操作。本篇将逐步讲解 JavaScript 的异步模型:回调函数、Promise 和 async/await。你将学会使用 Fetch API 与服务器交互,理解事件循环的基本概念,并在此基础上搭建一个实时汇率查询小应用,跨入前后端数据交互的大门。
一、同步与异步
JavaScript 是单线程语言,一个时间只能做一件事。如果任务耗时很长(比如网络请求),同步执行会阻塞页面,导致卡顿。因此,浏览器提供了异步 API(定时器、AJAX、事件监听等),这些操作交给浏览器其他线程处理,完成后通过回调函数通知 JS 主线程。
二、回调函数与回调地狱
最基础的异步模式就是回调函数。
<!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>当多个异步任务需要顺序执行时,会出现“回调地狱”,代码层层嵌套,难以维护:
// 模拟地狱 getUser(userId, function(user) { getPosts(user.id, function(posts) { getComments(posts[0].id, function(comments) { // ... }); }); });三、Promise:优雅的异步方案
ES6 引入的 Promise 对象,代表一个异步操作的最终完成或失败。
基本创建和消费:
<!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 的语法糖,使得异步代码写起来像同步,可读性大大提高。
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 完成。
<!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 请求:
fetch('https://api.github.com/users/octocat') .then(res => res.json()) .then(data => console.log(data)) .catch(err => console.error(err));POST 请求:
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)执行,循环往复。
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 应放在后端,这里示例仅作学习。
<!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/await、fetch、DOM 操作和事件监听,体现了真实项目的开发流程。
总结: 我们从回调函数讲到 Promise 再到 async/await,这是现代 JavaScript 异步编程的主线。掌握了 Fetch API,你就打开了与服务器通信的大门。事件循环的知识帮助你写出更可靠、更高效的代码。此刻,你已具备了前后端交互的核心技能。接下来,我们将进行最后的拼图:面向对象、模块化,并运用全套知识打造一个大型项目——我的任务管家。
如果这篇文章帮你解决了实操上的困惑,别忘记点击点赞、分享,也可以留言告诉我你遇到的其它问题,我会尽快回复。动手练习是掌握编程最快的方法,请务必亲手敲一遍本文的所有示例代码,并截图保存你的成果。你的关注是我坚持原创和细节共享的力量来源,谢谢大家。
