什么是异步编程?
异步编程允许代码在等待某些操作(如网络请求、文件读取等)完成时继续执行其他任务,而不是阻塞整个程序。这对于构建响应式的Web应用至关重要。
JavaScript的单线程模型
JavaScript是单线程语言,这意味着它一次只能执行一个任务。异步编程通过事件循环(Event Loop)机制实现非阻塞操作。
同步 vs 异步
| 特性 | 同步编程 | 异步编程 |
|---|---|---|
| 执行方式 | 顺序执行,阻塞后续代码 | 非阻塞,后续代码立即执行 |
| 性能 | 可能造成界面冻结 | 更好的响应性和用户体验 |
| 复杂度 | 简单直观 | 需要处理回调、Promise等 |
| 适用场景 | 简单计算、快速操作 | I/O操作、网络请求、定时任务 |
JavaScript事件循环
事件循环是JavaScript处理异步操作的核心机制,它由调用栈、任务队列和微任务队列组成。
事件循环流程
- 执行同步代码(调用栈)
- 处理微任务队列(Promise回调等)
- 处理宏任务队列(setTimeout、事件回调等)
- 重复上述过程
事件循环示例
console.log('1. 同步代码开始');
// 宏任务
setTimeout(() => {
console.log('6. 宏任务执行');
}, 0);
// Promise(微任务)
Promise.resolve()
.then(() => {
console.log('4. 微任务1执行');
return '结果';
})
.then((value) => {
console.log('5. 微任务2执行,收到:', value);
});
console.log('2. 同步代码继续');
// 另一个微任务
queueMicrotask(() => {
console.log('3. 微任务执行');
});
console.log('7. 同步代码结束');
// 输出顺序:
// 1. 同步代码开始
// 2. 同步代码继续
// 7. 同步代码结束
// 4. 微任务1执行
// 3. 微任务执行
// 5. 微任务2执行,收到: 结果
// 6. 宏任务执行
微任务 vs 宏任务
| 类型 | 示例 | 执行时机 |
|---|---|---|
| 微任务 | Promise.then/catch/finally, queueMicrotask, MutationObserver | 在当前任务结束后,下一个任务开始前 |
| 宏任务 | setTimeout, setInterval, setImmediate, I/O操作, UI渲染 | 在下一个事件循环迭代中 |
回调函数(Callback)
回调函数是异步编程的基础,它是一个在另一个函数执行完成后被调用的函数。
回调函数示例
// 简单的回调函数
function fetchData(callback) {
setTimeout(() => {
const data = { name: '张三', age: 25 };
callback(null, data); // 第一个参数通常是错误对象
}, 1000);
}
// 使用回调函数
fetchData((error, data) => {
if (error) {
console.error('错误:', error);
} else {
console.log('数据:', data);
}
});
// Node.js风格回调(错误优先)
function readFile(path, callback) {
// 模拟文件读取
setTimeout(() => {
const success = Math.random() > 0.2;
if (success) {
callback(null, `文件 ${path} 的内容`);
} else {
callback(new Error(`无法读取文件: ${path}`));
}
}, 500);
}
// 使用示例
readFile('/path/to/file.txt', (err, data) => {
if (err) {
console.error('错误:', err.message);
return;
}
console.log('文件内容:', data);
});
// 回调地狱示例(不推荐)
function getUserData(userId, callback) {
getUser(userId, (error, user) => {
if (error) return callback(error);
getPosts(user.id, (error, posts) => {
if (error) return callback(error);
getComments(posts[0].id, (error, comments) => {
if (error) return callback(error);
callback(null, { user, posts, comments });
});
});
});
}
回调地狱(Callback Hell): 多层嵌套的回调函数会导致代码难以阅读和维护,这就是所谓的"回调地狱"或"金字塔厄运"。
Promise
Promise是ES6引入的异步编程解决方案,解决了回调地狱的问题。Promise表示一个异步操作的最终完成(或失败)及其结果值。
Promise的生命周期
- pending(待定):初始状态,既没有被兑现,也没有被拒绝
- fulfilled(已兑现):操作成功完成
- rejected(已拒绝):操作失败
Promise基础
// 创建Promise
const promise = new Promise((resolve, reject) => {
// 异步操作
setTimeout(() => {
const success = Math.random() > 0.5;
if (success) {
resolve('操作成功!'); // 状态变为fulfilled
} else {
reject('操作失败!'); // 状态变为rejected
}
}, 1000);
});
// 使用Promise
promise
.then(result => {
console.log('成功:', result);
return result.toUpperCase(); // 可以返回新值
})
.then(uppercaseResult => {
console.log('处理后的结果:', uppercaseResult);
})
.catch(error => {
console.error('失败:', error);
})
.finally(() => {
console.log('操作完成'); // 无论成功失败都会执行
});
// Promise链式调用
function getUser(userId) {
return new Promise((resolve) => {
setTimeout(() => resolve({ id: userId, name: '用户' + userId }), 500);
});
}
function getUserPosts(userId) {
return new Promise((resolve) => {
setTimeout(() => resolve([`用户${userId}的帖子1`, `用户${userId}的帖子2`]), 300);
});
}
getUser(1)
.then(user => {
console.log('用户:', user);
return getUserPosts(user.id); // 返回新的Promise
})
.then(posts => {
console.log('用户的帖子:', posts);
})
.catch(error => {
console.error('错误:', error);
});
Promise的静态方法
Promise工具方法
// Promise.resolve - 创建已解决的Promise
const resolvedPromise = Promise.resolve('立即解决的值');
resolvedPromise.then(value => console.log(value)); // "立即解决的值"
// Promise.reject - 创建已拒绝的Promise
const rejectedPromise = Promise.reject('立即拒绝的原因');
rejectedPromise.catch(reason => console.error(reason)); // "立即拒绝的原因"
// Promise.all - 所有Promise都成功
const promises = [
Promise.resolve('结果1'),
Promise.resolve('结果2'),
Promise.resolve('结果3')
];
Promise.all(promises)
.then(results => {
console.log('所有结果:', results); // ['结果1', '结果2', '结果3']
})
.catch(error => {
console.error('有一个失败:', error); // 如果任一Promise失败
});
// Promise.race - 第一个完成(成功或失败)的Promise
Promise.race([
fetch('/api/slow'),
fetch('/api/fast'),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('超时')), 5000)
)
])
.then(result => console.log('最快的结果:', result))
.catch(error => console.error('错误:', error));
// Promise.allSettled - 所有Promise都完成(无论成功失败)
Promise.allSettled([
Promise.resolve('成功'),
Promise.reject('失败'),
Promise.resolve('另一个成功')
])
.then(results => {
results.forEach(result => {
if (result.status === 'fulfilled') {
console.log('成功:', result.value);
} else {
console.log('失败:', result.reason);
}
});
});
// Promise.any - 第一个成功的Promise(ES2021)
Promise.any([
Promise.reject('错误1'),
Promise.resolve('成功1'),
Promise.resolve('成功2')
])
.then(firstSuccess => {
console.log('第一个成功的结果:', firstSuccess); // "成功1"
})
.catch(error => {
console.error('全部失败:', error);
});
async/await
async/await是ES2017引入的语法糖,让异步代码看起来像同步代码,基于Promise构建,但提供了更清晰的语法。
async/await用法
// async函数总是返回Promise
async function fetchUserData(userId) {
try {
const user = await getUser(userId); // 等待Promise解决
const posts = await getPosts(user.id); // 等待另一个Promise
const comments = await getComments(posts[0].id);
return { user, posts, comments }; // 自动包装为Promise
} catch (error) {
console.error('获取数据失败:', error);
throw error; // 重新抛出错误
}
}
// 使用async函数
fetchUserData(1)
.then(data => console.log('用户数据:', data))
.catch(error => console.error('错误:', error));
// 立即执行async函数
(async function() {
try {
const data = await fetchUserData(1);
console.log('立即执行结果:', data);
} catch (error) {
console.error('立即执行错误:', error);
}
})();
// 在类方法中使用
class UserService {
async getUserProfile(userId) {
const user = await this.fetchUser(userId);
const posts = await this.fetchPosts(userId);
return { user, posts };
}
async fetchUser(userId) {
// 模拟API调用
return new Promise(resolve => {
setTimeout(() => resolve({ id: userId, name: `用户${userId}` }), 500);
});
}
}
// 并行执行多个异步操作
async function fetchAllData(userId) {
// 使用Promise.all并行执行
const [user, posts, settings] = await Promise.all([
getUser(userId),
getPosts(userId),
getSettings(userId)
]);
return { user, posts, settings };
}
// 在循环中使用await
async function processUsers(userIds) {
const results = [];
for (const id of userIds) {
const user = await getUser(id); // 顺序执行
results.push(user);
}
return results;
}
// 并行处理循环(更高效)
async function processUsersParallel(userIds) {
const promises = userIds.map(id => getUser(id));
return await Promise.all(promises); // 并行执行
}
async/await最佳实践
- 总是使用try/catch处理错误
- 使用Promise.all并行执行独立操作
- 避免不必要的await
- 在循环中谨慎使用await,考虑性能影响
高级异步模式
1. 异步生成器
异步生成器
// 异步生成器函数
async function* asyncGenerator() {
let i = 0;
while (i < 3) {
// 模拟异步操作
await new Promise(resolve => setTimeout(resolve, 1000));
yield i++;
}
}
// 使用for await...of循环
(async () => {
for await (const value of asyncGenerator()) {
console.log(value); // 0, 1, 2 (每秒输出一个)
}
})();
// 分页数据的异步生成器
async function* paginatedData(url) {
let page = 1;
let hasMore = true;
while (hasMore) {
const response = await fetch(`${url}?page=${page}`);
const data = await response.json();
yield data.items;
hasMore = data.hasMore;
page++;
}
}
// 使用分页数据
(async () => {
for await (const items of paginatedData('/api/data')) {
console.log('收到数据:', items);
// 处理每一页的数据
}
})();
2. 取消异步操作
可取消的Promise
// 创建可取消的Promise
function cancellablePromise(executor) {
let cancel;
const promise = new Promise((resolve, reject) => {
cancel = (reason = '操作已取消') => {
reject(new Error(reason));
};
executor(resolve, reject, () => cancel);
});
return { promise, cancel };
}
// 使用示例
const { promise, cancel } = cancellablePromise((resolve, reject, onCancel) => {
const timer = setTimeout(() => resolve('操作完成'), 5000);
// 注册取消回调
onCancel(() => {
clearTimeout(timer);
console.log('清理资源...');
});
});
// 在3秒后取消
setTimeout(() => {
cancel('用户取消了操作');
}, 3000);
promise
.then(result => console.log('成功:', result))
.catch(error => console.error('失败:', error.message));
// 使用AbortController(现代方法)
function fetchWithTimeout(url, options = {}, timeout = 5000) {
const controller = new AbortController();
const { signal } = controller;
const timeoutId = setTimeout(() => {
controller.abort();
}, timeout);
return fetch(url, {
...options,
signal
}).finally(() => {
clearTimeout(timeoutId);
});
}
// 使用示例
fetchWithTimeout('/api/data', {}, 3000)
.then(response => response.json())
.then(data => console.log('数据:', data))
.catch(error => {
if (error.name === 'AbortError') {
console.log('请求超时');
} else {
console.error('其他错误:', error);
}
});
3. 重试机制
带重试的异步操作
// 带指数退避的重试函数
async function retryWithBackoff(operation, maxRetries = 3, baseDelay = 1000) {
let lastError;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error;
if (attempt === maxRetries) {
break;
}
// 指数退避:延迟时间随尝试次数增加
const delay = baseDelay * Math.pow(2, attempt);
console.log(`尝试 ${attempt + 1} 失败,${delay}ms后重试...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
throw lastError;
}
// 使用示例
async function fetchData() {
return retryWithBackoff(
async () => {
const response = await fetch('/api/unstable');
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
},
3, // 最多重试3次
1000 // 基础延迟1秒
);
}
fetchData()
.then(data => console.log('最终成功:', data))
.catch(error => console.error('最终失败:', error.message));
实践练习
👆 请点击上方按钮进行演示操作
选择不同的演示按钮来探索JavaScript异步编程的各种功能和用法
性能优化和最佳实践
1. 避免常见的性能陷阱
性能提示:
- 使用Promise.all并行执行独立操作
- 避免在循环中使用await,考虑使用Promise.all
- 合理使用缓存减少重复请求
- 设置适当的超时时间
- 使用防抖和节流控制高频操作
2. 错误处理策略
全面的错误处理
// 包装异步操作,提供更好的错误信息
async function executeWithContext(operation, context = '') {
try {
return await operation();
} catch (error) {
// 添加上下文信息
error.context = context;
error.timestamp = new Date().toISOString();
console.error(`操作失败 [${context}]:`, error);
throw error;
}
}
// 使用示例
async function loadPageData() {
const [user, posts] = await Promise.all([
executeWithContext(() => fetchUser(123), '获取用户信息'),
executeWithContext(() => fetchPosts(123), '获取用户帖子')
].map(p => p.catch(error => {
// 单个失败不影响其他操作
console.error('部分数据加载失败:', error.context);
return null;
})));
return { user, posts };
}
// 全局错误处理
window.addEventListener('unhandledrejection', event => {
console.error('未处理的Promise拒绝:', event.reason);
// 可以在这里报告错误到监控系统
event.preventDefault();
});
浏览器兼容性
| 特性 | Chrome | Firefox | Safari | Edge | IE |
|---|---|---|---|---|---|
| Promise | 32+ | 29+ | 8+ | 12+ | 11- |
| async/await | 55+ | 52+ | 10.1+ | 15+ | 11- |
| Promise.allSettled | 76+ | 71+ | 13+ | 79+ | 11- |
| Promise.any | 85+ | 79+ | 14+ | 85+ | 11- |
兼容性提示: 对于不支持现代异步特性的旧浏览器,可以使用Babel等转译工具,或引入polyfill如core-js。