Nodejs 事件模块
一、概述提示Node.js 的核心设计理念是事件驱动而 events 模块正是这一设计的基石与核心载体。事实上Node.js 中几乎所有核心模块如 http、fs、stream 等都以 Events 模块为基础构建因此它们本身也都继承了 EventEmitter 的特性具备事件监听、触发的核心能力。什么是 eventsEvents 发布订阅模式Publish-Subscribe你可以监听一个事件订阅你可以触发一个事件发布触发时所有监听该事件的函数都会执行events 与 EventEmitterNodejs 是基于事件驱动的异步操作架构内置 events模块events 模块提供了EventEmitter类Nodejs 中很多内置核心模块继承EventEmitter类二、快速入门// 1. 引入 events 模块constEventEmitterrequire(events);// 2. 创建事件实例constemitternewEventEmitter();2.1 EventEmitter 常用 API 完整清单1. on(eventName, listener) / addListener添加当事件被触发时的回调函数emitter.on(msg,(data1,data2){console.log(收到消息1,data1);});emitter.on(msg,(data1,data2){console.log(收到消息2,data2);});2. emit(eventName, […args])触发事件按照注册的顺序同步调用每个事件监听器emitter.emit(msg,Hello Node.js,你好 Node.js);3. once(eventName, listener)添加当事件在注册之后首次被触发时调用的回调函数emitter.once(pay,(){console.log(支付成功只会执行一次);});emitter.emit(pay);// 执行emitter.emit(pay);// 不执行4. off(eventName, listener) / removeListener移除特定的监听器functioncallback(){console.log(可移除的监听);}emitter.on(test,callback);emitter.off(test,callback);// 移除成功5. removeAllListeners([eventName])移除所有事件监听器emitter.removeAllListeners(msg);6. listenerCount(eventName)查看某个事件有多少个监听函数console.log(emitter.listenerCount(msg));7. listeners(eventName)获取某个事件的所有监听函数数组console.log(emitter.listeners(msg));8. prependListener(eventName, listener)把监听函数插到队列最前面执行emitter.prependListener(test,(){console.log(我最先执行);});9. prependOnceListener(eventName, listener)插到最前面 只执行一次10. eventNames()获取当前实例所有监听的事件名数组console.log(emitter.eventNames());// [ hello, msg, test ]11. setMaxListeners(n)设置最大监听数默认 10 个//超过会报警告防止内存泄漏。emitter.setMaxListeners(20);12. getMaxListeners()获取当前最大监听数三、继承 EventEmitter面向对象必备实际开发中我们不会直接用 emitter而是让自己的类拥有事件能力。constEventEmitterrequire(events);classUserextendsEventEmitter{login(){console.log(登录中...);this.emit(loginSuccess,张三);// 触发事件}}constusernewUser();user.on(loginSuccess,(name){console.log(name,登录成功);});user.login();四、this 指向问题事件回调里的 this 默认指向 EventEmitter 实例。emitter.on(test,function(){console.log(thisemitter);// true});五、浏览器中的事件环1. 事件环的核心目的浏览器的 JavaScript 引擎是单线程同一时间只能执行一段代码但浏览器本身是多线程的如网络请求线程、定时器线程、DOM 事件线程等。为了避免单线程阻塞比如等待网络请求完成时页面卡死就需要事件环来协调「同步代码」和「异步代码」的执行顺序实现“非阻塞”的异步效果。一句话总结事件环 浏览器的「异步任务调度器」负责让单线程高效处理异步操作。2. 核心概念调用栈、任务队列理解事件环必须先掌握两个核心组件二者配合完成异步任务的调度1调用栈Call Stack用于执行同步代码遵循「后进先出」LIFO原则——代码执行时同步任务依次压入栈中执行完成后依次弹出栈。如果遇到异步任务不会直接执行而是交给对应的浏览器线程处理自身继续执行后续同步代码。// 调用栈为空准备处理异步任务2任务队列Task Queue用于存放异步任务的回调函数当异步操作如定时器、网络请求、DOM 事件完成后其回调函数会被推入对应的任务队列等待调用栈空闲后执行。任务队列分为两类优先级不同这是重点也是易踩坑点宏任务Macro Task优先级较低常见类型setTimeout、setInterval定时器DOM 事件如 click、resize网络请求如 fetch、XMLHttpRequestscript 标签整体代码微任务Micro Task优先级较高会在当前宏任务执行完成后、下一个宏任务执行前清空所有微任务。常见类型Promise.then()、Promise.catch()、Promise.finally()注意Promise 本身是同步的只有其回调是微任务async/await本质是 Promise 的语法糖await 后面的代码会被包装成微任务queueMicrotask()专门创建微任务的 API浏览器事件环的执行流程核心步骤事件环的执行是一个循环往复的过程核心流程可总结为 5 步记牢就能解决所有相关面试题执行调用栈中所有同步代码遇到异步任务时交给对应浏览器线程处理不阻塞当前执行。同步代码执行完毕调用栈为空。清空所有微任务按顺序执行微任务队列中的回调直到队列清空。执行一个宏任务从宏任务队列中取出第一个任务压入调用栈执行完毕。重复步骤 2-4循环往复这就是“事件环”的由来。六、Node.js 中的事件环1. Node.js 事件环的核心目的Node.js 的 JavaScript 引擎同样是单线程但 Node.js 主要用于后端开发需处理大量 I/O 操作如文件读取、网络请求、数据库操作。这些 I/O 操作是阻塞性的若等待其完成再执行后续代码会导致效率极低。事件环的核心作用协调 Node.js 中的同步代码、异步代码尤其是 I/O 异步的执行顺序将 I/O 操作交给系统内核处理主线程继续执行其他代码待 I/O 完成后再通过事件环触发回调实现「非阻塞 I/O」提升 Node.js 程序的并发性能。一句话总结Node.js 事件环 后端异步任务调度器核心解决「单线程处理多 I/O 操作」的效率问题与 Events 模块配合实现事件驱动架构。2. 核心基础libuv 与事件环的关系Node.js 的事件环由 libuv 库提供底层支持libuv 是一个跨平台的异步 I/O 库其核心功能就是管理事件环的6个阶段以及处理 I/O 操作、定时器等异步任务是 Node.js 异步能力的基石。关键关联Events 模块的 EventEmitter 类其事件触发、回调执行最终都会依赖 Node.js 事件环的调度二者协同构成 Node.js 事件驱动的核心。3. 核心结构事件环的6个阶段按执行顺序Node.js 事件环会循环执行以下6个阶段每个阶段都有自己的任务队列存放该阶段需执行的回调函数只有当前阶段的队列清空后才会进入下一个阶段顺序不可颠倒。其中前4个阶段为核心阶段后2个阶段为辅助阶段1timers 阶段定时器阶段核心功能执行 setTimeout()、setInterval() 设定的回调函数。注意点该阶段并非严格按照定时器设定的时间精准执行而是在设定时间到达后将回调函数推入该阶段队列等待当前阶段执行若前序阶段耗时较长会出现延迟。2pending callbacks 阶段待定回调阶段核心功能执行上一轮事件环中未执行完的 I/O 回调主要是一些系统层面的 I/O 回调如 TCP 连接错误回调等属于 Node.js 内部优化的回调处理阶段。3idle, prepare 阶段空闲/准备阶段核心功能仅 Node.js 内部使用用于事件环的准备工作开发者无需关注也无法在该阶段添加自定义回调。4poll 阶段轮询阶段I/O 核心阶段核心功能处理大部分 I/O 异步回调如文件读取、网络请求、数据库操作等是 Node.js 事件环中最核心的阶段分为两种情况若 poll 队列不为空依次执行队列中的所有 I/O 回调直到队列清空或达到该阶段的执行上限。若 poll 队列为空会等待新的 I/O 事件触发或等待 timers 阶段设定的定时器到期随后进入下一个阶段check 阶段。5check 阶段检查阶段核心功能执行 setImmediate() 设定的回调函数。关键setImmediate() 是 Node.js 特有的异步 API其回调专门在 check 阶段执行优先级高于 timers 阶段的回调若在 poll 阶段为空时触发会立即进入 check 阶段。6close callbacks 阶段关闭回调阶段核心功能执行关闭类回调如 socket.close()、fs.close() 的回调当一个资源被关闭时其回调会被推入该阶段队列执行完毕后事件环进入下一轮循环。4. Node.js 中的微任务队列重点易踩坑与浏览器事件环不同Node.js 的微任务分为两类队列优先级不同且会在「每个事件环阶段执行完毕后」统一执行而非每个宏任务后这是 Node.js 事件环与浏览器事件环的核心区别之一。1微任务队列分类及优先级从高到低nextTick 队列仅存放 process.nextTick() 的回调函数优先级最高会在当前事件环阶段执行完毕后优先于所有其他微任务执行。其他微任务队列存放 Promise.then/catch/finally、queueMicrotask() 等回调函数优先级低于 nextTick 队列在 nextTick 队列清空后执行。2微任务执行时机核心规则每个事件环阶段timers、pending callbacks、poll、check、close callbacks执行完毕后都会先清空 nextTick 队列再清空其他微任务队列然后才进入下一个阶段。注意idle, prepare 阶段执行完毕后不会执行微任务因该阶段为内部使用无开发者自定义回调。5. Node.js 事件环的完整执行流程核心步骤结合6个阶段和微任务队列完整执行流程可总结为7步记牢可轻松应对面试题执行调用栈中的所有同步代码遇到异步任务定时器、I/O、微任务等按类型分别推入对应队列。进入 timers 阶段执行该阶段队列中所有到期的 setTimeout/setInterval 回调执行完毕后清空 nextTick 队列 → 清空其他微任务队列。进入 pending callbacks 阶段执行上一轮未完成的 I/O 回调执行完毕后清空 nextTick 队列 → 清空其他微任务队列。进入 idle, prepare 阶段执行 Node.js 内部准备工作无开发者回调直接进入下一个阶段。进入 poll 阶段执行所有 I/O 回调执行完毕后清空 nextTick 队列 → 清空其他微任务队列若 poll 队列为空等待新 I/O 或 timers 到期再进入下一个阶段。进入 check 阶段执行所有 setImmediate 回调执行完毕后清空 nextTick 队列 → 清空其他微任务队列。进入 close callbacks 阶段执行所有关闭类回调执行完毕后清空 nextTick 队列 → 清空其他微任务队列随后事件环进入下一轮循环重复步骤2-7。Node.js 事件环 vs 浏览器事件环核心区别总结对比维度Node.js 事件环浏览器事件环核心模型基于 libuv 的 6 个阶段循环执行宏任务 微任务 简单模型微任务分类2类nextTick 队列高优先级、其他微任务队列1类所有微任务优先级一致微任务执行时机每个事件环阶段结束后先清 nextTick再清其他微任务每个宏任务执行完毕后清空所有微任务特有 APIprocess.nextTick()、setImmediate()无仅通用宏/微任务 API核心用途处理后端 I/O 异步提升并发性能处理前端异步DOM 事件、定时器等保证页面流畅