人人都会AI编程

同步 / 异步 API、Promise 化用法

更新时间:2026-07-10

fs 模块是 Node.js 中使用频率最高的内置模块之一,但它提供的 API 有三种调用风格:同步回调异步以及基于 Promise 的异步。理解这三种方式的差异、适用场景以及如何相互转换,是掌握文件操作的基础。

三种调用方式概览

Node.js 的 fs 模块几乎对每一个文件操作都提供了同步和异步两个版本,而在 Node.js 10 之后,又通过 fs/promises 提供了官方的 Promise 接口。开发者可以根据需要灵活选择。

| 调用方式 | 函数命名特征 | 返回值 | 错误处理 | 是否阻塞 |
|---------|-------------|--------|---------|---------|
| 同步 | 以 Sync 结尾(如 readFileSync) | 直接返回数据 | 抛出异常(try/catch) | |
| 回调异步 | 无 Sync 后缀,接受回调函数 | undefined | 通过回调的第一个参数传递错误 | |
| Promise 异步 | 位于 fs/promises,函数名无 Sync | Promise 对象 | 通过 catchtry/catch (await) 捕获 | |

下面通过一个读取 JSON 配置文件的例子来演示这三种方式。

1. 同步 API:fs.readFileSync

const fs = require('fs');
const path = require('path');

function readConfigSync() {
  try {
    const raw = fs.readFileSync(path.join(__dirname, 'config.json'), 'utf-8');
    return JSON.parse(raw);
  } catch (err) {
    console.error('读取配置文件失败:', err.message);
    return {};
  }
}

const config = readConfigSync();
console.log(config.port);

同步 API 的优点是代码直观,适合在程序初始化阶段使用,例如在服务启动前加载配置文件。如果在请求处理过程中使用同步 API,会因为阻塞事件循环而导致并发能力急剧下降,应严格避免。

2. 回调异步 API:fs.readFile

fs.readFile('config.json', 'utf-8', (err, data) => {
  if (err) {
    console.error('读取失败:', err);
    return;
  }
  try {
    const config = JSON.parse(data);
    console.log(config.port);
  } catch (parseErr) {
    console.error('解析 JSON 失败:', parseErr);
  }
});

这是 Node.js 早期最常见的写法,遵循 错误优先回调(Error-first Callback)约定:回调的第一个参数是错误对象(没有错误时为 null),第二个参数是结果数据。这种方式不会阻塞事件循环,但当需要多个异步操作顺序执行时,容易陷入“回调地狱”。

3. Promise 异步 API:fs/promises

从 Node.js 10 开始,fs/promises 提供了返回 Promise 的文件操作接口,可以直接与 async/await 配合使用:

const fsp = require('fs/promises');

async function readConfigAsync() {
  try {
    const raw = await fsp.readFile('config.json', 'utf-8');
    return JSON.parse(raw);
  } catch (err) {
    console.error('读取失败:', err);
    return {};
  }
}

// 使用
readConfigAsync().then(config => {
  console.log(config.port);
});

这种写法兼具同步代码的清晰性和异步代码的非阻塞特性,是目前推荐的主流用法。fs/promises 中所有方法都返回 Promise,可以非常方便地配合 Promise.all 进行并发操作:

const [users, products] = await Promise.all([
  fsp.readFile('users.json', 'utf-8').then(JSON.parse),
  fsp.readFile('products.json', 'utf-8').then(JSON.parse),
]);

将回调风格 API Promise 化

在实际项目中,除了 fs 模块,还有许多第三方库仍然使用错误优先的回调风格。为了统一使用 async/await,Node.js 在 util 模块中提供了 promisify 工具函数,可以将遵循错误优先回调的异步函数转换为返回 Promise 的函数。

const fs = require('fs');
const util = require('util');

// 将 fs.readFile 转换为 Promise 版本
const readFilePromise = util.promisify(fs.readFile);

async function getContent(filePath) {
  try {
    const data = await readFilePromise(filePath, 'utf-8');
    return data;
  } catch (err) {
    console.error('读取错误:', err);
  }
}

util.promisify 要求原函数的大致签名是 (arg1, arg2, ..., callback),且回调函数的第一个参数是 error。转换后,返回的新函数除了不再接受回调参数外,其他参数与原函数一致。

你也可以对一个对象上的多个方法批量转换:

const fs = require('fs');
const util = require('util');

const fsAsync = {
  readFile: util.promisify(fs.readFile),
  writeFile: util.promisify(fs.writeFile),
  readdir: util.promisify(fs.readdir),
};

但更推荐直接使用 fs/promises,因为它是官方维护的,类型定义更完善,且在 Node.js 12+ 中功能已与回调版本对齐。

选型建议:何时用同步、何时用异步?

  • 服务器请求处理:绝对不要使用同步 API。一个 readFileSync 会让整个事件循环停顿,造成所有请求响应变慢。
  • 启动时的一次性操作:如加载配置文件、初始化全局变量,可以使用同步 API,因为此时服务还未接受请求,阻塞不会影响并发性能。
  • 工具脚本与命令行程序:短生命周期的脚本使用同步 API 通常更简单,不必处理异步流程。
  • 日常业务逻辑:优先使用 fs/promises + async/await,代码可读性高,且性能符合要求。

真实的注意事项

  1. 大文件读取readFilereadFileSync 会一次性将文件内容加载到内存,操作几百 MB 以上的大文件时可能导致内存溢出。此时应使用流式处理(fs.createReadStream),这将在“流(Stream)与背压原理”章节详细讲解。
  2. 权限与路径问题:同步和异步版本都可能因权限不足、文件不存在等原因失败,务必处理错误。缺失文件时错误码为 ENOENT,可在生产日志中根据错误码分类处理。
  3. utf-8 编码:不传编码时,读取到的是 Buffer 对象,转为字符串需指定编码。fs/promisesreadFile 如果不传编码,也返回 Buffer
  4. 性能差异微乎其微:对于小文件,同步和异步本身的运行时间差异极小(主要耗时在磁盘 I/O),但阻塞带来的事件循环延迟是绝对不能接受的,这就是服务环境下必须用异步的根本原因。

掌握了这三种调用方式,你就已经能够应对绝大多数文件操作场景。接下来的小节我们将继续深入 fs 模块的其他高级用法,包括文件流、目录操作和文件监听等。