面试题目:关于nodeJs

147 阅读4分钟

一、请问下node 里的模块是什么

Node 中的模块包括两类:

核心模块在node的源代码的编译过程中,编译进了二进制执行文件。在node进程启动时,部分核心模块就被直接加载进内存中,所以引入核心模块时,文件定位和编译执行都省略了,并且在路径分析中优先判断,所以他的加载速度是最快的

引用模块的方法有:

  1. require: commonj
  2. import: es6
  3. process.binding: 引入C++模块
  4. internalBinding: 原生模块中js引入C++模块(不对用户开发)

模块引入的步骤-require、import

  1. 路径分析
  2. 文件定位
  3. 编译执行

二、请介绍一下 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;
  • 所以会直接按照顺序执行即可;