NodeJS有难度的⾯试题(能答对⼏个)1、Node模块机制
1.1 请介绍⼀下node⾥的模块是什么
Node中,每个⽂件模块都是⼀个对象,它的定义如下:
function Module(id, parent) {
this.id = id;
this.parent = parent;
this.filename = null;
this.loaded = false;
this.children = [];
}
var module = new Module(filename, parent);
所有的模块都是 Module 的实例。可以看到,当前模块(module.js)也是 Module 的⼀个实例。
1.2 请介绍⼀下require的模块加载机制
这道题基本上就可以了解到⾯试者对Node模块机制的了解程度基本上⾯试提到
1、先计算模块路径
2、如果模块在缓存⾥⾯,取出缓存
3、加载模块
4、的输出模块的exports属性即可
// require 其实内部调⽤ Module._load ⽅法
Module._load = function(request, parent, isMain) {
/
/ 计算绝对路径
var filename = Module._resolveFilename(request, parent);
// 第⼀步:如果有缓存,取出缓存
var cachedModule = Module._cache[filename];
if (cachedModule) {
ports;
// 第⼆步:是否为内置模块
if (ists(filename)) {
quire(filename);
}
/********************************这⾥注意了**************************/
/
/ 第三步:⽣成模块实例,存⼊缓存
// 这⾥的Module就是我们上⾯的1.1定义的Module
var module = new Module(filename, parent);
nodejs到底是干嘛用的呢Module._cache[filename] = module;
/********************************这⾥注意了**************************/
// 第四步:加载模块
// 下⾯的module.load实际上是Module原型上有⼀个⽅法叫Module.prototype.load
try {
module.load(filename);
hadException = false;
} finally {
if (hadException) {
delete Module._cache[filename];
}
}
// 第五步:输出模块的exports属性
ports;
};
接着上⼀题继续发问
1.3 加载模块时,为什么每个模块都有__dirname,__filename属性呢,new Module的时候我们看到1.1部分没有这两个属性的,那么这两个属性是从哪⾥来的
// 上⾯(1.2部分)的第四步module.load(filename)
// 这⼀步,module模块相当于被包装了,包装形式如下
// 加载js模块,相当于下⾯的代码(加载node模块和json模块逻辑不⼀样)
(function (exports, require, module, __filename, __dirname) {
// 模块源码
// 假如模块代码如下
var math = require('math');
exports.area = function(radius){
return Math.PI * radius * radius
}
});
也就是说,每个module⾥⾯都会传⼊__filename, __dirname参数,这两个参数并不是module本⾝就有的,是外界传⼊的1.4 我们知道node导出模块有两种⽅式,⼀种是=xxx和ports={}有什么区别吗
exports其实就是ports
其实1.3问题的代码已经说明问题了,接着我引⽤廖雪峰⼤神的讲解,希望能讲的更清楚
很多时候,你会看到,在Node环境中,有两种⽅法可以在⼀个模块中输出变量:
⽅法⼀:对ports赋值:
// hello.js
function hello() {
console.log('Hello, world!');
}
function greet(name) {
console.log('Hello, ' + name + '!');
}
hello: hello,
greet: greet
};
⽅法⼆:直接使⽤exports:
// hello.js
function hello() {
console.log('Hello, world!');
}
function greet(name) {
console.log('Hello, ' + name + '!');
}
function hello() {
console.log('Hello, world!');
}
exports.hello = hello;
< = greet;
但是你不可以直接对exports赋值:
// 代码可以执⾏,但是模块并没有输出任何变量:
exports = {
hello: hello,
greet: greet
};
如果你对上⾯的写法感到⼗分困惑,不要着急,我们来分析Node的加载机制:
⾸先,Node会把整个待加载的hello.js⽂件放⼊⼀个包装函数load中执⾏。在执⾏这个load()函数前,Node准备好了module变量:
var module = {
id: 'hello',
exports: {}
};
load()函数最终返回ports:
var load = function (exports, module) {
// hello.js的⽂件内容
...
// load函数返回:
ports;
};
var exported = ports, module);
也就是说,默认情况下,Node准备的exports变量和ports变量实际上是同⼀个变量,并且初始化为空对象{},于是,我们可以写:exports.foo = function () { return 'foo'; };
exports.bar = function () { return 'bar'; };
也可以写:
换句话说,Node默认给你准备了⼀个空对象{},这样你可以直接往⾥⾯加东西。
但是,如果我们要输出的是⼀个函数或数组,那么,只能给ports赋值:
给exports赋值是⽆效的,因为赋值后,ports仍然是空对象{}。
结论
如果要输出⼀个键值对象{},可以利⽤exports这个已存在的空对象{},并继续在上⾯添加新的键值;
如果要输出⼀个函数或数组,必须直接对ports对象赋值。
所以我们可以得出结论:直接对ports赋值,可以应对任何情况:
foo: function () { return 'foo'; }
};
或者:
最终,我们强烈建议使⽤ports = xxx的⽅式来输出模块变量,这样,你只需要记忆⼀种⽅法。
2、Node的异步I/O
本章的答题思路⼤多借鉴于朴灵⼤神的《深⼊浅出的NodeJS》
2.1 请介绍⼀下Node事件循环的流程
在进程启动时,Node便会创建⼀个类似于while(true)的循环,每执⾏⼀次循环体的过程我们成为Tick。
每个Tick的过程就是查看是否有事件待处理。如果有就取出事件及其相关的回调函数。然后进⼊下⼀个循环,如果不再有事件处理,就退出进程。
2.2 在每个tick的过程中,如何判断是否有事件需要处理呢?
1. 每个事件循环中有⼀个或者多个观察者,⽽判断是否有事件需要处理的过程就是向这些观察者询问是否有要处理的事
件。
2. 在Node中,事件主要来源于⽹络请求、⽂件的I/O等,这些事件对应的观察者有⽂件I/O观察者,⽹络I/O的观察者。
3. 事件循环是⼀个典型的⽣产者/消费者模型。异步I/O,⽹络请求等则是事件的⽣产者,源源不断为Node提供不同类型的
事件,这些事件被传递到对应的观察者那⾥,事件循环则从观察者那⾥取出事件并处理。
4. 在windows下,这个循环基于IOCP创建,在*nix下则基于多线程创建
2.3 请描述⼀下整个异步I/O的流程
3、V8的垃圾回收机制
3.1 如何查看V8的内存使⽤情况
使⽤Usage(),返回如下
{
rss: 4935680,
heapTotal: 1826816,
heapUsed: 650472,
external: 49879
}
heapTotal和heapUsed代表V8的内存使⽤情况。external代表V8管理的,绑定到Javascript的C++对象的内存使⽤情况。rss,驻留集⼤⼩, 是给这个进程分配了多少物理内存(占总分配内存的⼀部分) 这些物理内存中包含堆,栈,和代码段。
3.2 V8的内存限制是多少,为什么V8这样设计
64位系统下是1.4GB, 32位系统下是0.7GB。因为1.5GB的垃圾回收堆内存,V8需要花费50毫秒以上,做⼀次⾮增量式的垃圾回收甚⾄要1秒以上。这是垃圾回收中引起Javascript线程暂停执⾏的事件,在这样的花销下,应⽤的性能和影响⼒都会直线下降。
3.3 V8的内存分代和回收算法请简单讲⼀讲
在V8中,主要将内存分为新⽣代和⽼⽣代两代。新⽣代中的对象存活时间较短的对象,⽼⽣代中的对象存活时间较长,或常驻内存的对象。
3.3.1 新⽣代
新⽣代中的对象主要通过Scavenge算法进⾏垃圾回收。这是⼀种采⽤复制的⽅式实现的垃圾回收算法。它将堆内存⼀份为⼆,每⼀部分空间成为semispace。在这两个semispace空间中,只有⼀个处于使⽤中,另⼀个处于闲置状态。处于使⽤状态的semispace空间称为From空间,处于闲置状态的空间称为To空间。
当开始垃圾回收的时候,会检查From空间中的存活对象,这些存活对象将被复制到To空间中,⽽⾮存活对象占⽤的空间将会被释放。完成复制后,From空间和To空间发⽣⾓⾊对换。
应为新⽣代中对象的⽣命周期⽐较短,就⽐较适合这个算法。
当⼀个对象经过多次复制依然存活,它将会被认为是⽣命周期较长的对象。这种新⽣代中⽣命周期较长的对象随后会被移到⽼⽣代中。
3.3.2 ⽼⽣代
⽼⽣代主要采取的是标记清除的垃圾回收算法。与Scavenge复制活着的对象不同,标记清除算法在标记阶段遍历堆中的所有对象,并标记活着的对象,只清理死亡对象。活对象在新⽣代中只占叫⼩部分,死对象在⽼⽣代中只占较⼩部分,这是为什么采⽤标记清除算法的原因。
3.3.3 标记清楚算法的问题
主要问题是每⼀次进⾏标记清除回收后,内存空间会出现不连续的状态
这种内存碎⽚会对后续内存分配造成问题,很可能出现需要分配⼀个⼤对象的情况,这时所有的碎⽚空间都⽆法完成此次分配,就会提前触发垃圾回收,⽽这次回收是不必要的。
为了解决碎⽚问题,标记整理被提出来。就是在对象被标记死亡后,在整理的过程中,将活着的对象往⼀端移动,移动完成后,直接清理掉边界外的内存。
3.3.4 哪些情况会造成V8⽆法⽴即回收内存
闭包和全局变量
3.3.5 请谈⼀下内存泄漏是什么,以及常见内存泄漏的原因,和排查的⽅法
什么是内存泄漏
内存泄漏(Memory Leak)指由于疏忽或错误造成程序未能释放已经不再使⽤的内存的情况。
如果内存泄漏的位置⽐较关键,那么随着处理的进⾏可能持有越来越多的⽆⽤内存,这些⽆⽤的内存变多会引起服务器响应速度变慢。
严重的情况下导致内存达到某个极限(可能是进程的上限,如 v8 的上限;也可能是系统可提供的内存上限)会使得应⽤程序崩溃。常见内存泄漏的原因内存泄漏的⼏种情况:
⼀、全局变量
a = 10;
//未声明对象。
global.b = 11;
//全局变量引⽤
这种⽐较简单的原因,全局变量直接挂在 root 对象上,不会被清除掉。
⼆、闭包
function out() {
const bigData = new Buffer(100);
inner = function () {
}
}
闭包会引⽤到⽗级函数中的变量,如果闭包未释放,就会导致内存泄漏。上⾯例⼦是 inner 直接挂在了 root 上,那么每次执⾏out 函数所产⽣的 bigData 都不会释放,从⽽导致内存泄漏。
需要注意的是,这⾥举得例⼦只是简单的将引⽤挂在全局对象上,实际的业务情况可能是挂在某个可以从 root 追溯到的对象上导致的。
三、事件监听
Node.js 的事件监听也可能出现的内存泄漏。例如对同⼀个事件重复监听,忘记移除(removeListener),将造成内存泄漏。这种情况很容易在复⽤对象上添加事件时出现,所以事件重复监听可能收到如下警告:
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论