You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Node.js事件处理机制及多次触发登录事件的文件写入异常排查

问题原因分析

你遇到的不稳定结果是异步竞态条件导致的,具体原因如下:

  1. EventEmitter的触发机制:login.emit()是同步执行的,连续调用四次emit时,四个login事件的监听器会被同时触发,各自独立启动异步操作,互相之间不会等待对方完成。
  2. 文件读写的竞态:四个监听器几乎同时执行fs.promises.readFile,此时userlist.txt里是空数组,所有监听器都读到空数组。随后每个监听器往数组里push自己的用户名,再执行writeFile。因为写操作是异步的,最终谁最后完成写操作,就会把自己的数组覆盖到文件里,导致最终结果只保留最后一个完成的用户名(或随机某一个,取决于异步任务的完成顺序)。
Node.js EventEmitter核心特性
  • 同步触发监听器:调用emit()时,会立即同步调用所有注册的监听器函数。如果监听器是异步函数(带async),emit()不会等待该异步函数完成,直接继续执行后续代码。
  • 无内置异步串行处理:EventEmitter本身不处理异步监听器的执行顺序,多个异步监听器会并行执行,涉及共享资源(比如文件)操作时,极易引发竞态问题。
解决方案

方案1:串行执行异步任务(推荐)

将用户添加逻辑封装为异步函数,通过await确保每次操作完成后再执行下一次,彻底避免竞态:

const fs = require('fs');

async function addUser(username) {
  try {
    const userList = await fs.promises.readFile("./userlist.txt", 'utf-8');
    const parsedUserList = JSON.parse(userList);
    parsedUserList.push(username);
    await fs.promises.writeFile("./userlist.txt", JSON.stringify(parsedUserList));
    console.log(`user logged in successfully... ${username}`);
  } catch (error) {
    console.log(error);
  }
}

// 串行执行所有添加操作
async function run() {
  await addUser("Jhon");
  await addUser("Smith");
  await addUser("Jos");
  await addUser("Test");
}

run();

方案2:基于EventEmitter实现串行队列

如果必须保留EventEmitter的使用方式,可以维护一个异步任务队列,确保每个事件的处理等待前一个完成:

const fs = require('fs');
const EventEmitter = require('events');
const login = new EventEmitter();

// 初始化空Promise作为队列起点
let taskQueue = Promise.resolve();

login.on("login", (username) => {
  // 将当前任务加入队列,等待前一个任务完成后执行
  taskQueue = taskQueue.then(async () => {
    try {
      const userList = await fs.promises.readFile("./userlist.txt", 'utf-8');
      const parsedUserList = JSON.parse(userList);
      parsedUserList.push(username);
      await fs.promises.writeFile("./userlist.txt", JSON.stringify(parsedUserList));
      console.log(`user logged in successfully... ${username}`);
    } catch (error) {
      console.log(error);
    }
  });
});

login.emit("login", "Jhon");    
login.emit("login", "Smith");
login.emit("login", "Jos");
login.emit("login", "Test");

方案3:文件锁(进阶)

针对多进程共享文件的复杂场景,可以使用fs.promises.flock实现文件锁,确保同一时间只有一个进程能读写文件,不过这种方式相对复杂,适合特定场景。

内容的提问来源于stack exchange,提问作者Alok Shaw

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 04:02:33