深入理解 Node.js 事件循环机制:实现异步非阻塞 I/O 的核心

在 Node.js 的技术世界里,事件循环机制是重中之重,它是实现异步非阻塞 I/O 操作的关键。简单来说,事件循环就像是一个忙碌的调度员,专门负责等待各种消息和事件,然后安排它们依次执行。有了这个机制,Node.js 在处理任务时,不会因为 I/O 操作(比如读取文件、网络请求这类耗时操作)而停下脚步,从而让应用运行得又快又稳。

事件循环的六个阶段

  1. Timers(定时器阶段) :这个阶段主要负责执行setTimeout和setInterval设置的定时器回调函数。比如说,你设置了一个setTimeout(() => console.log('时间到啦!'), 2000),意思是 2 秒后要执行这个打印操作。但实际上,由于计算机要同时处理很多事情,真正执行这个回调的时间可能会比 2 秒稍微多一点,只是尽量接近 2 秒。
  1. Pending Callback(待定回调阶段) :在这里,系统会执行那些延迟到下一轮循环才处理的 I/O 回调。比如,当你发起一个网络请求获取数据,服务器响应数据回来后,相关的处理函数就可能会在这个阶段执行。像下面这个简单的 HTTP 请求案例:
javascript 复制代码
const http = require('http');
http.get('http://example.com', (res) => {
    // 这个回调可能会在Pending Callback阶段执行
    let data = '';
    res.on('data', (chunk) => {
        data += chunk;
    });
    res.on('end', () => {
        console.log('获取到的数据:', data);
    });
}).on('error', (err) => {
    console.error('请求出错:', err);
});
  1. Idle prepare(空闲、准备阶段) :这个阶段主要是 Node.js 内部在使用,我们日常写业务代码时基本不会直接接触到。它就像是在任务执行间隙,系统自己在做一些准备工作,比如整理资源、调整状态,为接下来的任务执行做铺垫。
  1. Poll(轮询阶段) :Poll 阶段在整个事件循环中起着核心作用。Node.js 会在这个阶段主动去检查有没有新的 I/O 事件发生,然后执行相关的回调函数。例如,读取文件操作完成后,对应的回调就会在这个阶段执行。假设我们有一个读取文件的操作:
javascript 复制代码
const fs = require('fs');
fs.readFile('test.txt', 'utf8', (err, data) => {
    if (err) {
        console.error('读取文件出错:', err);
    } else {
        console.log('文件内容:', data);
    }
});

当文件读取完成,这个回调函数就会在 Poll 阶段被触发。如果此时还有其他类似的 I/O 回调在等待,事件循环会一直处理这些任务,直到没有可处理的任务或者达到了预先设定的时间限制。

  1. Check(检查阶段) :Check 阶段专门用来执行setImmediate设置的回调函数。当你希望某些操作在 I/O 操作完成后马上执行,就可以用setImmediate。比如:
javascript 复制代码
const fs = require('fs');
fs.readFile('test.txt', 'utf8', (err, data) => {
    if (err) {
        console.error('读取文件出错:', err);
    } else {
        setImmediate(() => {
            console.log('文件读取完成后,马上执行这个操作');
        });
    }
});

这里setImmediate的回调函数会在文件读取完成后的 Check 阶段执行。

  1. Close Callback(关闭回调阶段) :当应用程序进行关闭文件描述符、断开网络连接等操作时,对应的关闭回调函数就会在这个阶段执行。例如,关闭一个文件:
javascript 复制代码
const fs = require('fs');
const fd = fs.openSync('test.txt', 'r');
fs.close(fd, (err) => {
    if (err) {
        console.error('关闭文件出错:', err);
    } else {
        console.log('文件已成功关闭');
    }
});

fs.close的回调函数会在 Close Callback 阶段被触发,确保文件关闭后的清理工作能及时完成。

宏任务与微任务

在 Node.js 的事件循环里,任务分为宏任务和微任务两类。宏任务包括setTimeout、setInterval、setImmediate、各种 I/O 操作,还有整个脚本的执行过程等。微任务主要有process.nextTick以及Promise.then等。特别要注意的是,process.nextTick在所有微任务里优先级是最高的。

一般情况下,setTimeout和setImmediate的执行顺序是不确定的。这是因为它们的执行时间受很多因素影响,比如事件循环当时在做什么、系统忙不忙等。但是,在 I/O 操作比较多的场景下,setImmediate会比setTimeout先执行。这是因为setImmediate就是设计用来在 I/O 操作一完成就马上执行的,而setTimeout是按照设定的时间来触发。例如:

javascript 复制代码
const fs = require('fs');
fs.readFile('test.txt', 'utf8', (err, data) => {
    if (err) {
        console.error('读取文件出错:', err);
    } else {
        setTimeout(() => {
            console.log('setTimeout执行');
        }, 0);
        setImmediate(() => {
            console.log('setImmediate执行');
        });
    }
});

在这个读取文件的例子里,通常setImmediate的回调会先打印出 "setImmediate 执行",然后setTimeout的回调再打印 "setTimeout 执行"。

性能优化建议

  1. 避免嵌套使用 process.nextTick:process.nextTick优先级非常高,如果在代码里大量嵌套使用,就好像有个特别着急的人一直插队,其他任务就很难有机会执行,这会严重影响事件循环的正常运行。尤其是在高并发的情况下,程序可能会变得很慢,甚至没有响应。比如下面这个不好的示例:
javascript 复制代码
function badExample() {
    process.nextTick(() => {
        process.nextTick(() => {
            process.nextTick(() => {
                // 这里嵌套了很多层process.nextTick
                console.log('执行了很多层嵌套');
            });
        });
    });
}
  1. 合理使用 setImmediate 代替 process.nextTick:setImmediate也能实现把任务延迟执行的功能,而且它对事件循环的压力比较小。在很多实际应用场景中,用setImmediate可以更优雅地处理任务,避免因为任务安排不合理而出现性能问题,同时保证事件循环能顺畅运行。例如,我们可以把上面不好的示例改写成这样:
scss 复制代码
function goodExample() {
    setImmediate(() => {
        setImmediate(() => {
            setImmediate(() => {
                console.log('用setImmediate代替嵌套');
            });
        });
    });
}
  1. 避免同步操作阻塞事件循环:同步操作就像是在事件循环的道路上设置了一个路障,其他任务都得等着它完成才能继续前进。所以在写 Node.js 代码时,尽量把那些耗时的操作改成异步的。比如读取文件,不要用同步的fs.readFileSync,而是用异步的fs.readFile;发起网络请求也用异步的方式。像下面这样:
javascript 复制代码
// 不好的同步读取文件方式
// const data = fs.readFileSync('test.txt', 'utf8');
// console.log('同步读取的文件内容:', data);
// 好的异步读取文件方式
const fs = require('fs');
fs.readFile('test.txt', 'utf8', (err, data) => {
    if (err) {
        console.error('读取文件出错:', err);
    } else {
        console.log('异步读取的文件内容:', data);
    }
});

通过这种方式,事件循环在等待 I/O 操作完成的同时,还能去处理其他任务,大大提高了应用程序的整体处理效率。

关于 I/O

I/O(也就是输入输出)操作在 Node.js 应用中到处都有。从读取和写入文件,到创建 HTTP 服务器并处理客户端请求,再到连接数据库执行查询语句等,这些都是 I/O 操作。Node.js 的事件循环机制就是为了高效管理这些 I/O 操作而设计的。通过异步非阻塞的方式,在进行 I/O 等待时,应用程序还能及时响应其他任务,让整个应用的性能和用户体验都更好。

相关推荐
还是鼠鼠3 小时前
Node.js Express 处理静态资源
前端·javascript·vscode·node.js·json·express
阿陈陈陈8 小时前
【Node.js入门笔记12---npm包】
笔记·npm·node.js
coding随想10 小时前
scss报错Sass @import rules are deprecated and will be removed in Dart Sass 3.0.0
前端·node.js·sass·scss
yang_love101112 小时前
Webpack vs Vite:深度对比与实战示例,如何选择最佳构建工具?
前端·webpack·node.js
355984268550551 天前
医保服务平台 Webpack逆向
前端·webpack·node.js
不能只会打代码1 天前
六十天前端强化训练之第三十一天之Webpack 基础配置 大师级讲解(接下来几天给大家讲讲工具链与工程化)
前端·webpack·node.js
还是鼠鼠1 天前
Node.js 路由 - 初识 Express 中的路由
前端·vscode·前端框架·npm·node.js·express
神影天初1 天前
安装node,配置npm, yarn, pnpm, bun
node.js
9527!到!1 天前
nvm 命令的实际意义讲解
npm·node.js
QC七哥1 天前
picgo的vscode插件支持easyimage图床
node.js·visual studio code