Node.js 事件循环
事件循环是 Node.js 处理非阻塞 I/O 操作的核心机制,使得单线程能够高效处理多个并发请求。
Node.js 是基于单线程的 JavaScript 运行时,利用事件循环来处理异步操作,如文件读取、网络请求和数据库查询。
事件循环使得 Node.js 能够非阻塞地运行代码、处理多个连接、以及执行异步 I/O 操作。
事件循环使得 Node.js 能够处理大量并发的 I/O 操作而不会导致线程阻塞,这是 Node.js 高效处理并发请求的关键。
事件循环的阶段
事件循环分为多个阶段,每个阶段处理特定的任务。关键阶段如下:
- Timers:执行
setTimeout()
和setInterval()
的回调。 - I/O Callbacks:处理一些延迟的 I/O 回调。
- Idle, prepare:内部使用,不常见。
- Poll:检索新的 I/O 事件,执行与 I/O 相关的回调。
- Check:执行
setImmediate()
回调。 - Close Callbacks:处理关闭的回调,如
socket.on('close', ...)
。
事件循环的流程
- 任务进入事件循环队列。
- 事件循环按照阶段顺序进行处理,每个阶段有自己的回调队列。
- 事件循环会在
poll
阶段等待新的事件到达,如果没有事件,会检查其他阶段的回调。 - 如果
setImmediate()
和setTimeout()
都存在,setImmediate()
在check
阶段先执行,而setTimeout()
在timers
阶段执行。
实例
console.log('Timeout callback');
}, 0);
setImmediate(() => {
console.log('Immediate callback');
});
console.log('Main thread execution');
输出顺序:
Main thread execution
先打印。setImmediate()
和setTimeout()
的执行顺序取决于当前事件循环的状态,一般setImmediate()
会先执行。
宏任务与微任务
- 宏任务:
setTimeout
、setInterval
、setImmediate
、I/O 操作等。 - 微任务:
process.nextTick
、Promise.then
。
执行顺序:微任务优先级高于宏任务,会在当前阶段的回调结束后立即执行。
实例
console.log('Timeout callback');
}, 0);
Promise.resolve().then(() => {
console.log('Promise callback');
});
console.log('Main thread execution');
执行输出结果:
Main thread execution Promise callback Timeout callback
process.nextTick()
process.nextTick() 会在当前操作结束后、下一个阶段开始前执行微任务,优先级高于 Promise。
实例
console.log('Next tick callback');
});
console.log('Main thread execution');
输出:
Main thread execution Next tick callback
事件驱动程序
在 Node.js 中,事件驱动编程主要通过 EventEmitter 类来实现。
EventEmitter 是一个内置类,位于 events 模块中,通过继承 EventEmitter,你可以创建自己的事件发射器,并注册和触发事件。
通过这种机制,Node.js 可以高效地处理异步任务,即使在单线程的环境下也能实现并发处理。
整个事件驱动的流程就是这么实现的,非常简洁。有点类似于观察者模式,事件相当于一个主题(Subject),而所有注册到这个事件上的处理函数相当于观察者(Observer)。
基本概念:
- 事件:在程序中发生的动作或状态改变,例如一个文件读取完成或一个 HTTP 请求到达。
- 事件触发器:
EventEmitter
是 Node.js 的内置模块,用来发出和监听事件。 - 事件处理器:与事件关联的回调函数,事件发生时被调用。
事件驱动的流程:
- 注册事件:在程序中通过
EventEmitter
实例注册事件和对应的处理器。 - 触发事件:当指定的事件发生时,
EventEmitter
会触发该事件。 - 处理事件:事件循环会调度相应的回调函数来执行任务。
Node.js 有多个内置的事件,我们可以通过引入 events 模块,并通过实例化 EventEmitter 类来绑定和监听事件,如下实例:
// 引入 events 模块 var events = require('events'); // 创建 eventEmitter 对象 var eventEmitter = new events.EventEmitter();
以下程序绑定事件处理程序:
// 绑定事件及事件的处理程序 eventEmitter.on('eventName', eventHandler);
我们可以通过程序触发事件:
// 触发事件 eventEmitter.emit('eventName');
实例
创建 hello.js 文件,代码如下所示:
实例 1
const myEmitter = new EventEmitter();
// 注册事件处理器
myEmitter.on('greet', () => {
console.log('Hello, world!');
});
// 触发事件
myEmitter.emit('greet');
输出:
Hello, world!
创建 main.js 文件,代码如下所示:
实例 2
接下来让我们执行以上代码:
$ node main.js 连接成功。 数据接收成功。 程序执行完毕。
Node 应用程序是如何工作的?
在 Node 应用程序中,执行异步操作的函数将回调函数作为最后一个参数, 回调函数接收错误对象作为第一个参数。
接下来让我们来重新看下前面的实例,创建一个 input.txt ,文件内容如下:
菜鸟教程官网地址:www.runoob.com
创建 main.js 文件,代码如下:
var fs = require("fs"); fs.readFile('input.txt', function (err, data) { if (err){ console.log(err.stack); return; } console.log(data.toString()); }); console.log("程序执行完毕");
以上程序中 fs.readFile() 是异步函数用于读取文件。 如果在读取文件过程中发生错误,错误 err 对象就会输出错误信息。
如果没发生错误,readFile 跳过 err 对象的输出,文件内容就通过回调函数输出。
执行以上代码,执行结果如下:
程序执行完毕 菜鸟教程官网地址:www.runoob.com
接下来我们删除 input.txt 文件,执行结果如下所示:
程序执行完毕 Error: ENOENT, open 'input.txt'
因为文件 input.txt 不存在,所以输出了错误信息。
岳小弟
shu***zizuo2018@126.com
注:Node.js 是单进程单线程应用程序,但是通过事件和回调支持并发,所以性能非常高。
什么是单进程单线程?直接读到再去敲实例,根本不理解到底是什么意思。这个问题就必须讲下什么是进程,什么是线程。
进程:CPU执行任务的模块。线程:模块中的最小单元。
例举:cpu比作我们每个人,到饭点吃饭了。可以点很多菜(cpu中的进程):宫保鸡丁,鱼香肉丝,酸辣土豆丝。每样菜具体包含了哪些内容(cpu每个进程中的线程):宫保鸡丁(详情:黄瓜、胡萝卜、鸡肉、花生米)。而详情构成了宫保鸡丁这道菜,吃了以后不饿。就可以干活了,cpu中的进程里的线程也是同理。当线程完成自己的内容将结果返回给进程,进程返回给cpu的时候。cpu就能处理日常需求。
岳小弟
shu***zizuo2018@126.com
lu
bai***tar@gmail.com
首先说事件
事件就是需要 eventEmitter.on 去绑定一个事件 通过 eventEmitter.emit 去触发这个事件其次说的是 事件的 接收 和 发生 是分开的 就像 一个外卖店你可以不停的接受很多订单, 接受以后开始告诉厨师去做外卖, 做好的外卖对应的外送给每个用户,如果单线程的话那只能是接收一个订单, 做好以后在接收下一个外卖订单,明显效率非常低。
事件可以不停的接受不停的发生也是为了提高效率。
lu
bai***tar@gmail.com
Java开发老菜鸟
sam***@foxmail.com
1、eventEmitter.emit 是触发事件(事件请求),eventEmitter.on是绑定处理事件的处理器(事件处理),事件的请求和处理是分开的,所以是异步。
2、如果把下面两个例子写在一起执行:
你会发现,例子 2 先输出,例子 1 后输出,可以验证是异步的,因为例子 1 需要进行 IO 耗时较长,但是例子 2 是直接输出信息,耗时较短,在两者几乎同时执行的情况下,例子 2 优先执行完。
Java开发老菜鸟
sam***@foxmail.com
韩非
171***818@qq.com
事件处理例子执行顺序如下:
韩非
171***818@qq.com
junwind
865***609@qq.com
这一篇里,定义的匿名函数使用了函数名,其实不加更优:
或者直接用:
junwind
865***609@qq.com