摘要: 网页常常需要从服务器获取数据而无需刷新页面,这就需要异步操作。本篇将逐步讲解 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/await、fetch、DOM 操作和事件监听,体现了真实项目的开发流程。
总结: 我们从回调函数讲到 Promise 再到 async/await,这是现代 JavaScript 异步编程的主线。掌握了 Fetch API,你就打开了与服务器通信的大门。事件循环的知识帮助你写出更可靠、更高效的代码。此刻,你已具备了前后端交互的核心技能。接下来,我们将进行最后的拼图:面向对象、模块化,并运用全套知识打造一个大型项目------我的任务管家。
如果这篇文章帮你解决了实操上的困惑,别忘记点击点赞、分享 ,也可以留言告诉我你遇到的其它问题,我会尽快回复。动手练习是掌握编程最快的方法,请务必亲手敲一遍本文的所有示例代码,并截图保存你的成果。你的关注是我坚持原创和细节共享的力量来源,谢谢大家。