详细介绍一下Node.js中的事件循环机制
与浏览器事件循环的区别
- 阶段划分不同:虽然浏览器和 Node.js 都有事件循环,但浏览器的事件循环没有像 Node.js 这样明确划分成多个阶段,其主要分为宏任务队列和微任务队列。
- 宏任务种类不同:浏览器中的宏任务除了定时器、I/O 操作外,还包括用户交互事件、页面渲染等,而 Node.js 中的宏任务主要是与服务器端相关的操作,如文件 I/O、网络请求等。
- 微任务实现不同:在浏览器中,
MutationObserver也属于微任务,而在 Node.js 中没有这个。Node.js 中有process.nextTick(),这在浏览器中是没有的。
过程
- timers(定时器)阶段
- I/O callbacks(I/O 回调)阶段
- idle, prepare(闲置、准备)阶段
- poll(轮询)阶段
事件循环会不断地轮询操作系统,询问是否有新的 I/O 事件发生。如果有,就从 I/O 队列中取出事件并执行对应的回调函数。当没有新的 I/O 事件时,根据是否有
setImmediate()回调等条件来决定是否继续等待或进入下一个阶段。
- check(检查)阶段
- close callbacks(关闭回调)阶段
常用模块
HTTP 模块--文件模块--PATH 模块
模块的加载机制和缓存策略
Node.js 采用 CommonJS 规范来处理模块,其模块加载遵循一定的流程和规则,主要分为以下几种情况:
- 核心模块是 Node.js 自带的模块,如
fs、http、path等。这些模块在 Node.js 启动时就被加载到内存中,加载速度非常快。 - 文件模块加载
绝对路径:如果使用绝对路径引入模块,Node.js 会直接根据该路径查找并加载模块文件。 当使用相对路径引入模块时,Node.js 会根据当前执行文件的路径,结合相对路径找到对应的模块文件
相对路径:当使用相对路径引入模块时,Node.js 会根据当前执行文件的路径,结合相对路径找到对应的模块文件
扩展名处理:如果在
require时省略了扩展名,Node.js 会按照.js、.json、.node的顺序依次尝试查找对应的文件。
-
第三方模块加载
加载流程:
- Node.js 会从当前执行文件所在目录开始,逐级向上查找
node_modules目录。 - 在找到的
node_modules目录中,查找与模块名匹配的文件夹。 - 如果找到了对应的模块文件夹,会根据该模块的
package.json文件中的main字段指定的入口文件进行加载
JavaScript 运行时环境差异
| js | node |
|---|---|
全局对象是 window | global,挂在当前模块上 |
| DOM和BOM | 文件系统操作、网络通信、进程管理 |
| ES和CND和AMD | 用 CommonJS 规范,通过 require |
事件执行机制区别
浏览器环境:事件循环主要负责处理用户交互事件(如点击、滚动)、定时器(如 setTimeout、setInterval)和异步操作(如 fetch)等。它的任务队列分为宏任务队列和微任务队列,微任务会在当前宏任务执行结束后立即执行
Node.js 环境:事件循环更为复杂,分为多个阶段,包括 timers、I/O callbacks、idle, prepare、poll、check、close callbacks。每个阶段有不同的任务队列,定时器回调在 timers 阶段执行,setImmediate 回调在 check 阶段执行,process.nextTick 属于微任务,会在每个阶段切换前执行。
koa
请求先从外层中间件依次向内层传递,到达最内层中间件处理完核心业务逻辑后,再从内层向外层返回响应,整个过程呈现出一种先入后出的特点 采用pm2进行管理
其他
哪些会导致内存泄露
- 被视为全局变量,随着每次调用函数,数组不断增大,造成内存泄漏。
- 未清理的定时器和回调
- 这些闭包都引用了
largeArray,导致largeArray无法被垃圾回收,随着闭包数量的增加,内存占用会不断上升 - 缓存
- 这些闭包都引用了
largeArray,导致largeArray无法被垃圾回收,随着闭包数量的增加,内存占用会不断上升
如何在实际项目中选择使用 Object.prototype 还是 ES6 Proxy?
拦截范围
- Object.defineProperty() :只能劫持对象的单个属性,对于对象新增的属性无法自动劫持,需要手动调用
Vue.set()方法来实现响应式。对于数组,它只能劫持部分数组方法,如push、pop等变异方法,对于通过索引修改数组元素的操作无法劫持。 - ES6 Proxy:可以对整个对象进行代理,能够拦截对象的所有操作,包括属性的读取、设置、删除、枚举等。对于数组,
Proxy可以完全劫持数组的各种操作,不需要特殊处理。
性能
- Object.defineProperty() :在处理嵌套对象时,需要递归地对每个属性进行劫持,性能开销较大。尤其是在对象属性较多或嵌套层级较深时,初始化响应式系统的时间会明显增加。
- ES6 Proxy:
Proxy是对整个对象进行代理,不需要递归处理,性能相对较好。特别是在处理大型对象时,Proxy的优势更加明显。
代码复杂度
- Object.defineProperty() :由于需要手动处理属性的劫持和更新,代码逻辑相对复杂。特别是在处理嵌套对象和数组时,需要编写大量的辅助函数来实现响应式。
- ES6 Proxy:
Proxy的代码更加简洁,只需要定义一个代理对象和处理程序,就可以实现对对象的全面劫持。
react面试题
- 对于简单的组件内部状态,使用
useState或useReducer即可。 - 当需要在多层级组件之间共享少量状态时,React Context 是一个不错的选择。
- 对于大型复杂应用,涉及大量状态管理和复杂的状态逻辑,推荐使用 Redux 或 MobX。
ts面试题