您现在的位置是:网站首页 > 常见的异步陷阱文章详情

常见的异步陷阱

在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('处理完成');
    });
  });
});

这种金字塔式代码会导致:

  1. 错误处理重复
  2. 代码可读性差
  3. 难以维护

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);
});

解决方案:

  1. 使用worker线程
  2. 将任务拆分为异步批次
  3. 使用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));

我的名片

网名:~川~

岗位:console.log 调试员

坐标:重庆市-九龙坡区

邮箱:cc@qdcc.cn

沙漏人生

站点信息

  • 建站时间:2013/03/16
  • 本站运行
  • 文章数量
  • 总访问量
微信公众号
每次关注
都是向财富自由迈进的一步