JavaScript 异步编程:从回调到 async/await
JavaScript 是单线程语言,但实际开发中我们几乎无时无刻不在处理异步操作:网络请求、文件读写、定时器、事件监听……从最初的回调地狱,到 Promise 链,再到 async/await 的优雅语法,JavaScript 的异步编程经历了三次重大演进。这篇文章带你完整理解这三个阶段,掌握每种方案的精髓和适用场景。
一、JavaScript 为什么需要异步?
JavaScript 只有一个主线程。如果所有操作都是同步的,一个 3 秒的网络请求会冻结整个页面——UI 无法响应、动画停止、按钮点击无效。异步机制的核心思想是:遇到耗时操作,先让出来,等结果到了再处理。
// 同步代码:会阻塞主线程
// 模拟 3 秒的网络请求(同步阻塞)
const end = Date.now() + 3000;
while (Date.now() < end) {}
// 3 秒后才输出,期间页面卡死
// 异步代码:不阻塞
setTimeout(() => , 3000); // 3 秒后输出
// 立即输出,不等待
// 输出顺序:1, 3, 2
二、回调函数(Callback):异步的起点
2.1 基本模式
回调函数是最原始的异步处理方式:把一个函数当作参数传给异步操作,操作完成后调用这个函数。
function fetchData(url, callback) {
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.onload = function() {
if (xhr.status === 200) {
callback(null, JSON.parse(xhr.responseText));
} else {
callback(new Error('请求失败: ' + xhr.status));
}
};
xhr.onerror = function() {
callback(new Error('网络错误'));
};
xhr.send();
}
// 使用
fetchData('/api/user', function(err, data) {
if (err) {
console.error(err);
return;
}
});
2.2 回调地狱
当多个异步操作有依赖关系时,回调会层层嵌套,形成难以维护的"回调地狱":
// 回调地狱:可读性极差,难以维护
fetchData('/api/user/1', function(err, user) {
if (err) return console.error(err);
fetchData('/api/posts?userId=' + user.id, function(err, posts) {
if (err) return console.error(err);
fetchData('/api/comments?postId=' + posts[0].id, function(err, comments) {
if (err) return console.error(err);
// 如果还要继续嵌套...
});
});
});
回调地狱的问题不只是缩进难看,更严重的是:错误处理困难(每层都要 try-catch)、流程控制复杂(并发、串行难以表达)、调试困难(调用栈被异步打断)。
三、Promise:异步编程的救星
3.1 Promise 基础
Promise 是一个代表异步操作最终结果的对象。它有三种状态:
- pending:初始状态,既没有成功也没有失败
- fulfilled:操作成功
- rejected:操作失败
const promise = new Promise((resolve, reject) => {
// 异步操作
setTimeout(() => {
const success = true;
if (success) {
resolve('操作成功的结果'); // 将状态变为 fulfilled
} else {
reject(new Error('操作失败')); // 将状态变为 rejected
}
}, 1000);
});
promise
.then(result => ) // "操作成功的结果"
.catch(error => console.error(error)) // 捕获错误
.finally(() => ); // 清理工作
3.2 Promise 链式调用
Promise 的 .then() 返回一个新的 Promise,这就是链式调用的基础:
// 用 Promise 链替代回调地狱
fetch('/api/user/1')
.then(res => res.json())
.then(user => fetch('/api/posts?userId=' + user.id))
.then(res => res.json())
.then(posts => fetch('/api/comments?postId=' + posts[0].id))
.then(res => res.json())
.then(comments => )
.catch(err => console.error('任何一步出错都会被捕获:', err));
3.3 并发控制
Promise 的真正威力在于并发控制:
// Promise.all:所有都成功才成功,一个失败就失败
const [users, posts, comments] = await Promise.all([
fetch('/api/users').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
fetch('/api/comments').then(r => r.json())
]);
// Promise.allSettled:全部完成(不管成功失败)才返回
const results = await Promise.allSettled([
fetch('/api/users').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
fetch('/api/comments').then(r => r.json())
]);
// results = [{status: 'fulfilled', value: ...}, {status: 'rejected', reason: ...}]
// Promise.race:最快的一个(无论成功失败)
const fastest = await Promise.race([
fetch('/api/primary').then(r => r.json()),
fetch('/api/backup').then(r => r.json())
]);
// Promise.any:最快的一个成功结果
const firstSuccess = await Promise.any([
fetch('/api/server1').then(r => r.json()),
fetch('/api/server2').then(r => r.json())
]);
3.4 封装异步工具函数
/**
* 带超时的 fetch
*/
function fetchWithTimeout(url, options = {}, timeout = 5000) {
return Promise.race([
fetch(url, options),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('请求超时')), timeout)
)
]);
}
/**
* 带重试的 fetch
*/
async function fetchWithRetry(url, options = {}, retries = 3, delay = 1000) {
for (let i = 0; i <= retries; i++) {
try {
const res = await fetch(url, options);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.json();
} catch (err) {
if (i === retries) throw err;
await new Promise(r => setTimeout(r, delay * Math.pow(2, i))); // 指数退避
}
}
}
/**
* 串行执行(按顺序一个接一个)
*/
async function serial(tasks) {
const results = [];
for (const task of tasks) {
results.push(await task());
}
return results;
}
/**
* 并发控制(最多 n 个同时执行)
*/
async function concurrent(tasks, limit = 5) {
const results = [];
const executing = new Set();
for (const [i, task] of tasks.entries()) {
const p = Promise.resolve().then(() => task());
results[i] = p;
executing.add(p);
p.finally(() => executing.delete(p));
if (executing.size >= limit) {
await Promise.race(executing);
}
}
return Promise.all(results);
}
四、async/await:异步的终极形态
4.1 基本语法
async/await 是 Promise 的语法糖,让异步代码看起来像同步代码:
// async 函数总是返回 Promise
async function getUser(id) {
const response = await fetch(`/api/user/${id}`);
const data = await response.json();
return data;
}
// 使用
const user = await getUser(1); // 看起来像同步代码
4.2 错误处理
// 方式1:try-catch(推荐)
async function loadUserData(userId) {
try {
const user = await fetch(`/api/user/${userId}`).then(r => r.json());
const posts = await fetch(`/api/posts?userId=${userId}`).then(r => r.json());
return { user, posts };
} catch (error) {
console.error('加载用户数据失败:', error);
// 可以返回默认值,而不是让错误冒泡
return { user: null, posts: [] };
}
}
// 方式2:Go 风格的错误处理
function asyncWrapper(promise) {
return promise.then(data => [data, null]).catch(err => [null, err]);
}
async function loadData() {
const [users, usersErr] = await asyncWrapper(fetch('/api/users').then(r => r.json()));
if (usersErr) return console.error(usersErr);
const [posts, postsErr] = await asyncWrapper(fetch('/api/posts').then(r => r.json()));
if (postsErr) return console.error(postsErr);
return { users, posts };
}
4.3 实战:优雅的 API 请求层
class ApiClient {
constructor(baseURL) {
this.baseURL = baseURL;
this.defaultHeaders = {
'Content-Type': 'application/json',
};
}
setToken(token) {
this.defaultHeaders['Authorization'] = `Bearer ${token}`;
}
async request(method, path, data = null) {
const url = `${this.baseURL}${path}`;
const config = {
method,
headers: { ...this.defaultHeaders },
};
if (data && method !== 'GET') {
config.body = JSON.stringify(data);
}
try {
const response = await fetch(url, config);
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(error.message || `HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
if (error.name === 'TypeError') {
throw new Error('网络连接失败,请检查网络');
}
throw error;
}
}
get(path) { return this.request('GET', path); }
post(path, data) { return this.request('POST', path, data); }
put(path, data) { return this.request('PUT', path, data); }
delete(path) { return this.request('DELETE', path); }
}
// 使用
const api = new ApiClient('https://api.example.com');
api.setToken(localStorage.getItem('token'));
const users = await api.get('/users');
const newUser = await api.post('/users', { name: '张兴中' });
五、事件循环与执行顺序
理解事件循环(Event Loop)是掌握 JavaScript 异步的关键:
setTimeout(() => , 0);
Promise.resolve().then(() => );
// 输出顺序:1, 4, 3, 2
// 同步代码 → 微任务(Promise.then)→ 宏任务(setTimeout)
执行优先级:同步代码 > 微任务(Promise.then、queueMicrotask)> 宏任务(setTimeout、setInterval、I/O)。每个宏任务执行完后,会清空所有微任务,再执行下一个宏任务。
六、常见陷阱与最佳实践
- 忘记 await:async 函数中调用 async 函数必须 await,否则得到的是 Promise 对象
- 循环中的 await:需要串行时在循环内 await,需要并行时用
Promise.all() - 未捕获的 Promise:总是添加
.catch()或在 async 函数中 try-catch - 内存泄漏:取消不再需要的请求(AbortController),清理事件监听器
// 请求取消:AbortController
const controller = new AbortController();
async function fetchData() {
try {
const res = await fetch('/api/data', {
signal: controller.signal
});
return await res.json();
} catch (err) {
if (err.name === 'AbortError') {
return null;
}
throw err;
}
}
// 组件卸载时取消请求
// controller.abort();
异步编程是 JavaScript 的核心能力之一。理解事件循环、熟练使用 Promise 和 async/await,是每个前端开发者的必修课。记住:回调是基础,Promise 是核心,async/await 是日常。选择合适的工具,写出清晰、可维护的异步代码。