一、请问下node 里的模块是什么
Node 中的模块包括两类:
核心模块在node的源代码的编译过程中,编译进了二进制执行文件。在node进程启动时,部分核心模块就被直接加载进内存中,所以引入核心模块时,文件定位和编译执行都省略了,并且在路径分析中优先判断,所以他的加载速度是最快的
引用模块的方法有:
- require: commonj
- import: es6
- process.binding: 引入C++模块
- internalBinding: 原生模块中js引入C++模块(不对用户开发)
模块引入的步骤-require、import
- 路径分析
- 文件定位
- 编译执行
二、请介绍一下 require 的模块加载机制
1.模块优先从缓存中加载
2.内置模块加载机制优先级最高
3.自定义模块加载机制
导入自定义模块,必须有相对路径或绝对路径的表示符,例如./, ../等
在导入自定义模块时,如果省略了文件拓展名,node会按照以下顺序匹配对应的文件:
① 按照确切的文件名加载
② 匹配.js文件
③ 匹配.json文件
④ 匹配.node文件
4、第三方模块加载机制
从当前模块的父目录开始,尝试从 /node_modules 文件夹中加载第三方模块。
如果没有找到对应的第三方模块,则移动到再上一层父目录中,进行加载,直到文件系统的根目录。
5、目录作为模块
当把目录作为模块标识符,传递给 require() 进行加载的时候,有三种加载方式:
① 在被加载的目录下查找一个叫做 package.json 的文件,并寻找 main 属性,作为 require() 加载的入口
② 如果目录里没有 package.json 文件,或者 main 入口不存在或无法解析,则 Node.js 将会试图加载目录下的 index.js 文件。
③ 如果以上两步都失败了,则 Node.js 会在终端打印错误消息,报告模块的缺失:Error: Cannot find module 'xxx'
三、我们知道 node 导出模块有两种方式,一种是 exports.xxx=xxx 和 Module.exports={}有什么区别吗
可以简单理解为:
内部声明了一个名为exports变量:var exports = module.exports;
但最后暴露出去的是module.exports对象;
所以可以通过exports.XXX来修改module.exports.XXX,但是不能通过exports = {}来修改module.exports
四、请简单介绍下node事件循环(Event loop)
1.node运行机制
node使用V8作为js解析引擎,I/O处理使用了自己设计的libuv,libuv是一个基于事件的跨平台抽象层,封装了不同操作系统一些底层特性,对外提供统一的API,事件循环机制也是它里面的实现。
运行机制:
① V8引擎解析JavaScript脚本。
② 解析后的代码,调用Node API。
③ libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务的执行结果返回给V8引擎。
④ V8引擎再将结果返回给用户。
2.事件循环
libuv引擎中的事件循环分为 6 个阶段,循环运行。
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬───────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
① 定时器(timers):本阶段执行已经被 setTimeout() 和 setInterval() 的回调函数。
② 待定回调(pending callbacks):执行延迟到下一个循环迭代的 I/O 回调。
③ idle, prepare:仅系统内部使用。
④ 轮询(poll):检索新的 I/O 事件;执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,那些由计时器和 setImmediate() 调度的之外),其余情况 node 将在适当的时候在此阻塞。
⑤ 检测(check):setImmediate() 回调函数在这里执行。
⑥ 关闭的回调函数(close callbacks ):一些关闭的回调函数,如:socket.on('close', ...)。
五、node 在每个 tick 的过程中,如何判断是否有事件需要处理呢?
先给大家一个定义叫做tick,一个tick就是指一个事件周期。而process.nextTick()就是指在下一个事件循环tick开始之前,调用这个函数
Node的事件循环是一个我们编写的JavaScript代码和系统调用(file system、network等)之间的一个桥梁, 桥梁之间他们通过回调函数进行沟通的.
我们会发现从一次事件循环的Tick来说,Node的事件循环更复杂,它也分为微任务和宏任务:
- 宏任务(macrotask):setTimeout、setInterval、IO事件、setImmediate、close事件;
- 微任务(microtask):Promise的then回调、process.nextTick、queueMicrotask;
但是,Node中的事件循环不只是 微任务队列和 宏任务队列:
- 微任务队列:
-
- next tick queue:process.nextTick;
- other queue:Promise的then回调、queueMicrotask;
- 宏任务队列:
-
- timer queue:setTimeout、setInterval;
- poll queue:IO事件;
- check queue:setImmediate;
- close queue:close事件;
所以,在每一次事件循环的tick中,会按照如下顺序来执行代码:
- next tick microtask queue;
- other microtask queue;
- timer queue;
- poll queue;
- check queue;
- close queue;
// 面试题目一
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2')
}
console.log('script start')
setTimeout(function () {
console.log('setTimeout0')
}, 0)
setTimeout(function () {
console.log('setTimeout2')
}, 300)
setImmediate(() => console.log('setImmediate'));
process.nextTick(() => console.log('nextTick1'));
async1();
process.nextTick(() => console.log('nextTick2'));
new Promise(function (resolve) {
console.log('promise1')
resolve();
console.log('promise2')
}).then(function () {
console.log('promise3')
})
console.log('script end')
输出:
script start
async1 start
async2
promise1
promise2
script end
nextTick1
nextTick2
async1 end
promise3
setTimeout0
setImmediate
setTimeout2
// 面试题目二
setTimeout(() => {
console.log('setTimeout')
}, 0)
setImmediate(() => {
console.log('setImmediate')
})
输出
情况一:
setTimeout
setImmediate
情况二:
setImmediate
setTimeout
情况一:如果事件循环开启的时间(ms)是小于 setTimeout函数的执行时间的;
- 也就意味着先开启了event-loop,但是这个时候执行到timer阶段,并没有定时器的回调被放到入 timer queue中;
- 所以没有被执行,后续开启定时器和检测到有setImmediate时,就会跳过poll阶段,向后继续执行;
- 这个时候是先检测 setImmediate,第二次的tick中执行了timer中的 setTimeout;
情况二:如果事件循环开启的时间(ms)是大于 setTimeout函数的执行时间的;
- 这就意味着在第一次 tick中,已经准备好了timer queue;
- 所以会直接按照顺序执行即可;