您现在的位置是:网站首页 > 常见的异步陷阱文章详情
常见的异步陷阱
陈川
【
Node.js
】
43016人已围观
4803字
在Node.js开发中,异步编程是核心特性之一,但也容易陷入各种陷阱。回调地狱、未处理的Promise、事件循环阻塞等问题常常导致难以调试的bug或性能瓶颈。
回调地狱与解决方案
回调函数嵌套过深是早期Node.js开发中最典型的问题。例如读取文件后处理数据,再写入新文件:
fs.readFile('input.txt', 'utf8', (err, data) => {
if (err) throw err;
processData(data, (processed) => {
fs.writeFile('output.txt', processed, (err) => {
if (err) throw err;
console.log('处理完成');
});
});
});
这种金字塔式代码会导致:
- 错误处理重复
- 代码可读性差
- 难以维护
Promise链式调用
ES6引入的Promise可以扁平化嵌套:
readFilePromise('input.txt')
.then(processData)
.then(processed => writeFilePromise('output.txt', processed))
.then(() => console.log('处理完成'))
.catch(err => console.error('出错:', err));
async/await优化
ES2017的async/await让异步代码更像同步:
async function processFiles() {
try {
const data = await readFilePromise('input.txt');
const processed = await processData(data);
await writeFilePromise('output.txt', processed);
console.log('处理完成');
} catch (err) {
console.error('出错:', err);
}
}
Promise常见陷阱
忘记返回Promise
在then链中漏掉return会导致后续then接收undefined:
// 错误示例
somePromise()
.then(value => {
anotherAsync(value); // 没有return
})
.then(result => {
// result会是undefined
});
// 正确写法
somePromise()
.then(value => {
return anotherAsync(value);
})
未处理的拒绝
没有catch的Promise链可能静默失败:
function riskyOperation() {
return new Promise((resolve, reject) => {
setTimeout(() => reject(new Error('意外错误')), 1000);
});
}
// 控制台会报UnhandledPromiseRejectionWarning
riskyOperation().then(console.log);
应该始终添加catch:
riskyOperation()
.then(console.log)
.catch(err => console.error('捕获错误:', err));
事件循环阻塞
同步代码阻塞
长时间运行的同步代码会阻塞事件循环:
// 会阻塞所有其他请求处理
app.get('/compute', (req, res) => {
const result = cpuIntensiveTask(); // 同步计算
res.send(result);
});
解决方案:
- 使用worker线程
- 将任务拆分为异步批次
- 使用setImmediate分片处理
app.get('/compute', async (req, res) => {
const result = await runInWorker(cpuIntensiveTask);
res.send(result);
});
微任务与宏任务
理解执行顺序很重要:
setTimeout(() => console.log('宏任务'), 0);
Promise.resolve()
.then(() => console.log('微任务1'))
.then(() => console.log('微任务2'));
// 输出顺序:
// 微任务1
// 微任务2
// 宏任务
错误处理实践
回调中的错误传播
在回调风格中,错误可能被意外捕获:
function badPractice(callback) {
fs.readFile('missing.txt', (err, data) => {
if (err) throw err; // 会崩溃进程
callback(data);
});
}
应该将错误传递给回调:
function betterPractice(callback) {
fs.readFile('missing.txt', (err, data) => {
if (err) return callback(err);
callback(null, data);
});
}
async函数中的try/catch
async函数需要显式捕获错误:
async function fetchData() {
try {
const res = await fetch('https://api.example.com');
return await res.json();
} catch (err) {
// 网络错误或JSON解析错误都会到这里
console.error('请求失败:', err);
throw err; // 继续向上传播
}
}
资源泄漏问题
未关闭的文件描述符
忘记关闭文件会导致内存泄漏:
// 错误示例
fs.open('bigfile.txt', 'r', (err, fd) => {
if (err) throw err;
// 使用fd但忘记关闭
});
正确做法:
fs.open('bigfile.txt', 'r', (err, fd) => {
if (err) throw err;
try {
// 使用fd...
} finally {
fs.close(fd, (err) => {
if (err) console.error('关闭失败:', err);
});
}
});
事件监听器累积
不移除事件监听器会造成内存泄漏:
const server = net.createServer();
server.on('connection', (socket) => {
socket.on('data', (data) => {
// 处理数据
});
// 应该添加socket.on('close', ...)清理
});
解决方案:
server.on('connection', (socket) => {
const dataHandler = (data) => { /*...*/ };
socket.on('data', dataHandler);
socket.on('close', () => {
socket.removeListener('data', dataHandler);
});
});
并发控制
未限制的并发请求
同时发起大量请求可能耗尽资源:
// 可能造成系统过载
const urls = [...]; // 1000个URL
const promises = urls.map(url => fetch(url));
await Promise.all(promises);
使用p-limit等库控制并发:
import pLimit from 'p-limit';
const limit = pLimit(10); // 最大10个并发
const promises = urls.map(url =>
limit(() => fetch(url))
);
await Promise.all(promises);
竞态条件
异步操作的执行顺序不确定可能导致问题:
let balance = 100;
async function withdraw(amount) {
if (balance >= amount) {
await simulateNetworkLatency();
balance -= amount;
return true;
}
return false;
}
// 两个同时提现可能导致余额为负
await Promise.all([
withdraw(80),
withdraw(50)
]);
解决方案:使用锁或事务
const locks = new Map();
async function withLock(key, fn) {
while (locks.has(key)) {
await locks.get(key);
}
const promise = fn();
locks.set(key, promise);
try {
return await promise;
} finally {
locks.delete(key);
}
}
await withLock('account', () => withdraw(80));
上一篇: 异步性能优化技巧
下一篇: Buffer的设计初衷