JavaScript 异步编程入门
为什么要异步
JavaScript 是单线程语言,意味着同一时间只能执行一个任务。如果有耗时操作(如网络请求、文件读取),会阻塞后续代码执行。异步编程解决了这个问题:让耗时操作在后台执行,完成后通过回调通知主线程。
回调函数(Callback)
回调是最早的异步模式。将函数作为参数传递给异步操作,操作完成后调用该函数。
function fetchData(callback) {
setTimeout(() => {
callback('数据加载完成');
}, 1000);
}
fetchData((data) => {
console.log(data); // 1秒后输出
});
回调地狱:当多个异步操作嵌套时,代码变成金字塔状,难以阅读和维护。
getUser(id, (user) => {
getPosts(user.id, (posts) => {
getComments(posts[0].id, (comments) => {
// 嵌套越来越深
});
});
});
Promise
Promise 是一种更优雅的异步方案。它有三种状态:pending(进行中)、fulfilled(已完成)、rejected(已失败)。
const fetchData = new Promise((resolve, reject) => {
setTimeout(() => {
const success = true;
if (success) {
resolve('数据加载完成');
} else {
reject('加载失败');
}
}, 1000);
});
fetchData
.then((data) => console.log(data))
.catch((error) => console.error(error));
Promise 链式调用
getUser(id)
.then((user) => getPosts(user.id))
.then((posts) => getComments(posts[0].id))
.then((comments) => console.log(comments))
.catch((error) => console.error(error));
并发控制
// 全部完成
Promise.all([fetchUsers(), fetchPosts(), fetchComments()])
.then(([users, posts, comments]) => { ... });
// 任意一个完成
Promise.race([fetchData(), timeout(5000)])
.then((data) => console.log(data))
.catch(() => console.error('请求超时'));
// 全部完成(含失败)
Promise.allSettled([fetchUsers(), fetchPosts()])
.then((results) => results.forEach(r => console.log(r.status)));
async/await
async/await 是 Promise 的语法糖,让异步代码看起来像同步代码。
async function loadData() {
try {
const user = await getUser(id);
const posts = await getPosts(user.id);
const comments = await getComments(posts[0].id);
return comments;
} catch (error) {
console.error('加载失败:', error);
}
}
并行请求
async function loadDashboard() {
const [users, posts, comments] = await Promise.all([
fetchUsers(),
fetchPosts(),
fetchComments()
]);
return { users, posts, comments };
}
事件循环(Event Loop)
理解事件循环是掌握异步的关键。JS 执行顺序:
- 执行同步代码(调用栈)
- 遇到微任务(Promise.then、MutationObserver)→ 放入微任务队列
- 遇到宏任务(setTimeout、setInterval、I/O)→ 放入宏任务队列
- 同步代码执行完毕 → 清空微任务队列
- 取出一个宏任务执行 → 重复第 4 步
console.log('1'); // 同步
setTimeout(() => console.log('2'), 0); // 宏任务
Promise.resolve().then(() => console.log('3')); // 微任务
console.log('4'); // 同步
// 输出顺序:1, 4, 3, 2
总结
| 方式 | 优点 | 缺点 |
|---|---|---|
| 回调 | 简单直接 | 回调地狱,错误处理困难 |
| Promise | 链式调用,错误处理统一 | 理解门槛稍高 |
| async/await | 代码简洁,可读性强 | 需要配合 try/catch |
实战中推荐以 async/await 为主,需要并发控制时配合 Promise.all 使用。