异步编程实现方式
JavaScript 中常见的异步编程方案及代码示例
问题
JavaScript 中有哪些异步编程的实现方式?各自的特点是什么?
解答
1. 回调函数
最基础的异步方式,将函数作为参数传递。
// 模拟异步请求
function fetchData(callback) {
setTimeout(() => {
callback(null, { name: 'Alice' });
}, 1000);
}
// 使用回调
fetchData((err, data) => {
if (err) {
console.error(err);
return;
}
console.log(data); // { name: 'Alice' }
});
// 回调地狱示例
fetchData((err, data1) => {
fetchData((err, data2) => {
fetchData((err, data3) => {
// 嵌套过深,难以维护
});
});
});
2. Promise
解决回调地狱,支持链式调用。
// 创建 Promise
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ name: 'Alice' });
}, 1000);
});
}
// 链式调用
fetchData()
.then(data => {
console.log(data);
return fetchData();
})
.then(data => {
console.log(data);
})
.catch(err => {
console.error(err);
});
// 并行执行
Promise.all([fetchData(), fetchData()])
.then(([data1, data2]) => {
console.log(data1, data2);
});
// 竞速执行
Promise.race([fetchData(), fetchData()])
.then(data => {
console.log('最快返回:', data);
});
3. Generator
通过 yield 暂停执行,需要手动或借助执行器驱动。
// Generator 函数
function* fetchGenerator() {
const data1 = yield fetchData();
console.log(data1);
const data2 = yield fetchData();
console.log(data2);
return 'done';
}
// 简易执行器
function run(generator) {
const gen = generator();
function next(data) {
const result = gen.next(data);
if (result.done) return result.value;
// 假设 yield 的值是 Promise
result.value.then(next);
}
next();
}
run(fetchGenerator);
4. async/await
Generator 的语法糖,最直观的异步写法。
// async 函数
async function getData() {
try {
// await 等待 Promise 完成
const data1 = await fetchData();
console.log(data1);
const data2 = await fetchData();
console.log(data2);
// 并行执行
const [data3, data4] = await Promise.all([
fetchData(),
fetchData()
]);
return { data1, data2, data3, data4 };
} catch (err) {
console.error(err);
}
}
getData();
5. 事件监听
基于事件触发的异步模式。
// Node.js EventEmitter
const EventEmitter = require('events');
const emitter = new EventEmitter();
// 监听事件
emitter.on('data', (data) => {
console.log('收到数据:', data);
});
// 触发事件
setTimeout(() => {
emitter.emit('data', { name: 'Alice' });
}, 1000);
// 浏览器中的事件监听
document.addEventListener('click', (e) => {
console.log('点击位置:', e.clientX, e.clientY);
});
6. 发布订阅模式
解耦事件的发布者和订阅者。
// 简易发布订阅
class PubSub {
constructor() {
this.events = {};
}
// 订阅
subscribe(event, callback) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(callback);
// 返回取消订阅函数
return () => {
this.events[event] = this.events[event].filter(cb => cb !== callback);
};
}
// 发布
publish(event, data) {
if (!this.events[event]) return;
this.events[event].forEach(callback => callback(data));
}
}
// 使用
const pubsub = new PubSub();
const unsubscribe = pubsub.subscribe('userLogin', (user) => {
console.log('用户登录:', user);
});
pubsub.publish('userLogin', { name: 'Alice' });
unsubscribe(); // 取消订阅
各方式对比
| 方式 | 优点 | 缺点 |
|---|---|---|
| 回调函数 | 简单直接 | 回调地狱,错误处理麻烦 |
| Promise | 链式调用,统一错误处理 | 无法取消,语义不够直观 |
| Generator | 可暂停执行 | 需要执行器,使用复杂 |
| async/await | 同步写法,易读易写 | 需要 try/catch 处理错误 |
| 事件监听 | 解耦,适合多次触发 | 事件流难以追踪 |
| 发布订阅 | 完全解耦 | 过度使用导致代码难以理解 |
关键点
- 回调函数是基础,但嵌套过深会形成回调地狱
- Promise 通过链式调用解决嵌套问题,是现代异步的基石
- async/await 是目前最推荐的写法,代码最接近同步风格
- 事件监听和发布订阅适合一对多、多次触发的场景
- 实际开发中常组合使用,如 async/await + Promise.all 处理并发
目录