
JavaScript的异步编程是其核心特性之一,也是理解JavaScript运行机制的关键。下面我从几个方面详细介绍。
一、为什么需要异步编程?
JavaScript 是单线程语言,意味着同一时间只能做一件事。如果没有异步编程,当遇到耗时操作(如网络请求、文件读取、定时器)时,整个程序就会阻塞,导致页面卡死无法响应。异步编程就是为了解决这个问题,让耗时操作在后台执行,不影响主线程继续处理其他任务。
二、异步编程的演进历程
1. 回调函数(Callback)
最早的异步解决方案,将函数作为参数传入,在异步操作完成后执行。
// 定时器回调
setTimeout(() => {
console.log('2秒后执行');
}, 2000);
// 事件回调
button.addEventListener('click', () => {
console.log('按钮被点击');
});
// 传统的Ajax请求
function fetchData(callback) {
// 模拟异步请求
setTimeout(() => {
callback('获取到的数据');
}, 1000);
}
fetchData((data) => {
console.log(data);
});
☹ 问题:回调地狱
当多个异步操作有依赖关系时,就会出现嵌套过深的问题:
fetchData((data1) => {
processData(data1, (data2) => {
validateData(data2, (data3) => {
saveData(data3, (result) => {
console.log('最终结果', result);
});
});
});
});
这种代码难以阅读、难以维护,错误处理也很复杂。
2. Promise
Promise 是 ES6 引入的解决方案,表示一个异步操作的最终完成或失败。
// Promise 的基本使用
const promise = new Promise((resolve, reject) => {
// 异步操作
setTimeout(() => {
const success = true;
if (success) {
resolve('成功的数据');
} else {
reject('失败的原因');
}
}, 1000);
});
promise
.then(result => {
console.log('成功:', result);
return result + '处理';
})
.then(processed => {
console.log('处理后的结果:', processed);
})
.catch(error => {
console.log('失败:', error);
})
.finally(() => {
console.log('无论成功失败都会执行');
});
★ Promise 的关键特性:
状态不可逆:pending → fulfilled 或 pending → rejected,一旦改变就不能再变
链式调用 :通过 .then() 返回新的 Promise,解决了回调地狱问题
错误冒泡 :.catch() 可以捕获链中任意一个 Promise 的错误
// 用 Promise 改写上面的回调地狱
fetchData()
.then(data1 => processData(data1))
.then(data2 => validateData(data2))
.then(data3 => saveData(data3))
.then(result => console.log('最终结果', result))
.catch(error => console.error('出错了', error));
☛ Promise 的静态方法:
// Promise.all:所有都成功才成功,一个失败就失败
Promise.all([fetch1(), fetch2(), fetch3()])
.then(results => console.log('全部成功', results));
// Promise.race:谁先完成就用谁的结果
Promise.race([fetch1(), fetch2()])
.then(result => console.log('最快的那个', result));
// Promise.allSettled:等待所有都完成,无论成功或失败
Promise.allSettled([fetch1(), fetch2()])
.then(results => console.log('所有结果', results));
// Promise.any:任意一个成功就成功,全部失败才失败
Promise.any([fetch1(), fetch2()])
.then(result => console.log('第一个成功的', result));
3. Generator 函数(中间过渡方案)
Generator 可以暂停和恢复执行,配合 Promise 可以实现类似同步的异步代码,但使用起来不够直观。
function* asyncGenerator() {
const data1 = yield fetchData();
const data2 = yield processData(data1);
return data2;
}
// 需要手动执行器或使用 co 库
function run(generator) {
const it = generator();
function next(value) {
const result = it.next(value);
if (result.done) return result.value;
result.value.then(next);
}
next();
}
run(asyncGenerator);
4. async/await
ES2017 引入的语法糖,让异步代码写起来像同步代码。
// async 函数总是返回 Promise
async function getData() {
try {
const data1 = await fetchData();
const data2 = await processData(data1);
const data3 = await validateData(data2);
const result = await saveData(data3);
console.log('最终结果', result);
return result;
} catch (error) {
console.error('出错了', error);
throw error; // 可以继续抛出
}
}
// 调用
getData()
.then(result => console.log('完成', result))
.catch(error => console.error('捕获', error));
☺async/await 的优势:
1)代码更清晰,像同步代码一样顺序执行
2)使用 try/catch 统一处理错误,符合直觉
3)避免了 Promise 链式调用中可能出现的混乱
★ 注意事项:
// ❌ 错误用法:没有等待结果
async function badExample() {
fetchData(); // 没有 await,不会等待
console.log('这行会先执行');
}
// ✅ 正确用法
async function goodExample() {
const data = await fetchData();
console.log(data);
}
// ✅ 并发执行:用 Promise.all 优化
async function concurrentExample() {
// 同时发起,不用等待
const promise1 = fetchData1();
const promise2 = fetchData2();
// 等待全部完成
const [data1, data2] = await Promise.all([promise1, promise2]);
console.log(data1, data2);
}
三、事件循环(Event Loop)
理解异步编程,必须理解 JavaScript 的事件循环机制。
1. 核心概念
JavaScript 运行时包含:
1)调用栈:同步代码执行的地方
2)任务队列:存放异步任务回调的地方
3)事件循环:不断检查调用栈是否为空,为空则从队列中取任务执行
2. 宏任务与微任务
异步任务分为两种:
1)宏任务(MacroTask):
setTimeout、setInterval
I/O 操作
UI 渲染
setImmediate(Node.js)
2)微任务(MicroTask):
Promise 的 then/catch/finally
async/await 中 await 之后的代码
MutationObserver
queueMicrotask
3)执行顺序:
① 执行同步代码
② 清空所有微任务
③ 执行一个宏任务
④ 清空所有微任务
重复 3-4
console.log('1'); // 同步
setTimeout(() => {
console.log('2'); // 宏任务
}, 0);
Promise.resolve().then(() => {
console.log('3'); // 微任务
});
console.log('4'); // 同步
// 输出顺序:1, 4, 3, 2
// 更复杂的例子
async function async1() {
console.log('async1 start'); // 同步
await async2(); // await 后面的代码会变成微任务
console.log('async1 end'); // 微任务
}
async function async2() {
console.log('async2'); // 同步
}
console.log('script start'); // 同步
setTimeout(() => {
console.log('setTimeout'); // 宏任务
}, 0);
async1();
new Promise((resolve) => {
console.log('promise1'); // 同步
resolve();
}).then(() => {
console.log('promise2'); // 微任务
});
console.log('script end'); // 同步
// 输出顺序:
// script start
// async1 start
// async2
// promise1
// script end
// async1 end
// promise2
// setTimeout
四、实际应用场景
1. 网络请求
// 使用 fetch API
async function getUserInfo(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) throw new Error('请求失败');
const data = await response.json();
return data;
} catch (error) {
console.error('获取用户信息失败', error);
throw error;
}
}
2. 并发控制
// 限制并发数量的函数
async function limitConcurrency(tasks, limit) {
const results = [];
const executing = [];
for (const task of tasks) {
const p = Promise.resolve().then(() => task());
results.push(p);
if (limit <= tasks.length) {
const e = p.then(() => executing.splice(executing.indexOf(e), 1));
executing.push(e);
if (executing.length >= limit) {
await Promise.race(executing);
}
}
}
return Promise.all(results);
}
3. 轮询
async function poll(fn, interval, maxAttempts) {
let attempts = 0;
while (attempts < maxAttempts) {
try {
const result = await fn();
if (result) return result;
} catch (error) {
console.log(`第 ${attempts + 1} 次尝试失败`);
}
await new Promise(resolve => setTimeout(resolve, interval));
attempts++;
}
throw new Error('轮询超时');
}
// 使用
const data = await poll(
() => fetchData(),
1000, // 间隔1秒
5 // 最多尝试5次
);
五、常见陷阱与最佳实践
1. 忘记 await
// ❌ 错误:没有 await,函数立即返回 Promise 对象
async function getData() {
fetchData(); // 忘记 await
console.log('这行会先执行');
}
// ✅ 正确
async function getData() {
const data = await fetchData();
console.log(data);
}
2. 循环中的 await
// ❌ 串行执行,效率低
async function processItems(items) {
for (const item of items) {
await processItem(item); // 一个一个处理
}
}
// ✅ 并行执行
async function processItems(items) {
const promises = items.map(item => processItem(item));
await Promise.all(promises);
}
3. 错误处理
// ❌ 没有错误处理
async function riskyOperation() {
const data = await fetchData(); // 如果失败会抛出未捕获的异常
return data;
}
// ✅ 使用 try/catch
async function safeOperation() {
try {
const data = await fetchData();
return data;
} catch (error) {
console.error('操作失败', error);
return null; // 返回默认值
}
}
4. 避免 Promise 构造器滥用
// ❌ 不必要的 Promise 包装
function bad() {
return new Promise((resolve, reject) => {
fetchData()
.then(resolve)
.catch(reject);
});
}
// ✅ 直接返回 Promise
function good() {
return fetchData();
}
六、总结
JavaScript 异步编程经历了从回调函数到 Promise,再到 async/await 的演进,越来越符合人类的思维方式。
核心要点:
1)异步是单线程 JavaScript 的必然选择
2)理解事件循环是掌握异步的关键
3)优先使用 async/await 编写异步代码
4)注意微任务和宏任务的执行顺序
5)合理处理错误和并发
掌握了这些内容,我们就能够应对绝大部分 JavaScript 异步编程的场景了。