技术
·
阅读 8 分钟
JavaScript 异步编程完全指南
从回调函数到 Promise,再到 async/await,JavaScript 的异步编程经历了巨大演变。本文系统梳理各种异步模式的优缺点,帮助你写出更优雅的异步代码。
为什么需要异步?
JavaScript 是单线程语言,这意味着同一时间只能执行一个任务。如果遇到网络请求、文件读写等耗时操作,同步执行会阻塞后续代码,导致页面卡顿。异步编程让我们可以在等待耗时操作的同时继续执行其他代码。
回调函数(Callback)
最原始的异步处理方式。将一个函数作为参数传递给异步操作,当操作完成时调用这个函数。
function fetchData(url, callback) {
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.onload = () => callback(null, xhr.responseText);
xhr.onerror = () => callback(new Error('请求失败'));
xhr.send();
}
fetchData('/api/user', (err, data) => {
if (err) {
console.error(err);
return;
}
console.log(data);
});
回调函数简单直接,但当多个异步操作需要依次执行时,就会出现著名的"回调地狱":
fetchData('/api/user', (err, user) => {
fetchData(`/api/posts/${user.id}`, (err, posts) => {
fetchData(`/api/comments/${posts[0].id}`, (err, comments) => {
// 嵌套越来越深...
});
});
});
Promise
ES6 引入的 Promise 大大改善了异步代码的可读性。Promise 代表一个最终会完成或失败的异步操作。
function fetchData(url) {
return new Promise((resolve, reject) => {
fetch(url)
.then(res => res.json())
.then(data => resolve(data))
.catch(err => reject(err));
});
}
fetchData('/api/user')
.then(user => fetchData(`/api/posts/${user.id}`))
.then(posts => fetchData(`/api/comments/${posts[0].id}`))
.then(comments => console.log(comments))
.catch(err => console.error(err));
Promise 的链式调用将嵌套结构变成了扁平结构,代码更加清晰。
Async/Await
ES2017 带来的 async/await 语法糖,让异步代码看起来像同步代码一样直观:
async function loadData() {
try {
const user = await fetchData('/api/user');
const posts = await fetchData(`/api/posts/${user.id}`);
const comments = await fetchData(`/api/comments/${posts[0].id}`);
console.log(comments);
} catch (err) {
console.error('加载失败:', err);
}
}
并行执行
当多个异步操作互不依赖时,应该并行执行以提高性能:
// 串行 — 慢 ❌
const users = await fetchUsers();
const posts = await fetchPosts();
// 并行 — 快 ✅
const [users, posts] = await Promise.all([
fetchUsers(),
fetchPosts()
]);
总结
异步编程是 JavaScript 的核心能力之一。建议在新项目中优先使用 async/await,它兼顾了可读性和功能性。同时,理解底层的 Promise 和回调机制,能帮助你更好地调试和优化代码。