前言
最近在搭建B端Node服务,用的Egg,看源码&实际原理的时候,遇到一些问题,重新把NodeJS的基础知识整合一下。如果有什么表述不准确的地方,还希望大家指正一下~
本文主要内容:
- NodeJS简介
- 环境
- 从全局开始
- 全局对象 - global
- console 对象
- __dirname
- __filename
- EventLoop 事件轮询
- 模块化 CommonJS
- Process 进程
- Path 路径操作
- Buffer基本操作
- Stream
- FileSystem文件系统
- Events(核心模块)
- V8垃圾回收机制
1.NodeJS简介
node.js是⼀一个异步的事件驱动的JavaScript运⾏行行时 nodejs.org/en/
如何判断当前脚本运行在浏览器还是 node 环境中?
typeof window === 'undefined' ? 'node' : 'browser';
通过判断当前环境的 window 对象类型是否为 undefined,
如果是undefined,则说明当前脚本运行在node环境,否则说明运行在window环境。
node.js特性其实是JS的特性:
- ⾮阻塞I/O
- 事件驱动
- 高并发
node历史 — 为性能⽽生
- 并发处理
- 多进程
- 多线程
- 异步I/O
缺陷
- 不适用用CPU密集型
- CPU使用率较重、IO使用率较轻的应用——如视频编码、人工智能等,Node.js的优势无法发挥
- nodejs是单线程的,只支持单核CPU,无法充分利用多核 CPU 的性能
- 可靠性低,一旦代码某个环节崩溃,整个系统都崩溃
解决方案
- Nnigx反向代理,负载均衡,开多个进程,绑定多个端口;
- 单线程变成多线程,或者多进程单线程;
2.环境
- 环境搭建:安装 Node.js 解析器,解析执行 Node.js 代码(类似浏览器的 JS 解析器)。
- 执行环境(CLI & REPL):
CLI : 命令行接口
windows : cmd、power shell
macOS : 终端(terminal)
node 命令
输入 node 命令,进入REPL 环境(类似浏览器控制台) Read Eval Print Loop
REPL命令:
.break
.clear
.exit //退出node REPL终端 .help
.save //代码保存到1.js
.load //加载运行文件中的code .editor
- 命令行工具: 环境变量
windows
查看:set
设置:set 环境变量名称=值
删除:set环境变量名=
Linux/macOS
查看:echo $环境变量名称
设置:export 环境变量名称=值
解析指定文件
# 用指令指定程序打开指定文件
node [options] [V8 options] [script.js | -e "script" | -] [--] [arguments]
- options:选项
- V8 options:V8(node.js的javascript解析引擎)相关 选项
- [script.js | -e "script" | -]:要执行的脚本文件或内容
- arguments:参数
3. 从全局开始
3.1 全局对象 - global
- 类似浏览器全局对象 window,但是 node (ECMAScript) 环境中是没有window的(本质上,浏览器的 window 其实就是扩展自ECMAScript中的 global)
3.2 console 对象
Console 模块提供了一个简单的调试控制台,类似于
Web 浏览器提供的 JavaScript 控制台
.log([data][, ...args])
.info([data][, ...args]) -> .log()的别名
.debug(data[, ...args]) -> .log()的别名
.error([data][, ...args]) -> 与 .log() 类似
.warn([data][, ...args]) -> .error()的别名
.trace([message][, ...args])
3.3 __dirname
【文件夹】 当前文件(模块)所在目录,绝对路径,不包含文件名称
3.4 __filename
【文件】 当前文件(模块)的文件名称(包含文件绝对路径)
3.5 EventLoop 事件轮询
事件轮询是一直循环往复的,只有当任务队列为空时,才会停止循环,且在每一趟循环中,每一个环节都会有对应的操作。
process.nextTick()
// 进入每个阶段前都会执行,也就是当前(** 异步任务队列 **)阶段结束后立马执行nextTick,
// 然后进入下一阶段,包括nodejs启动的时候也会执行
setTimeout()/clearTimeout()
setInterval()/clearInterval()
setImmediate()/clearImmediate()
- Timer, 处理所有 setTimeout 和 setInterval 的回调
对于timers中队列的处理,setTimeout或setInterval中的函数都添加到队列里,同时记下来这些函数什么时间被调用到了调用时间就调用,没到时间就进入下一阶段,然后会停留在poll阶段
- i/o cycle
- Pending I/O Callback, 执行 I/O 回调,文件操作、网络操作等
除了以下操作的回调函数,其他的回调函数都在这个阶段执行。
setTimeout()和setInterval()的回调函数,(因为它在timer阶段执行)setImmediate()的回调函数,(因为它在Check阶段执行)- 用于关闭请求的回调函数,比如
socket.on('close', ...),(因为它在Close callbacks阶段执行)
-
Idle, Prepare 内部使用
-
Poll
这个阶段是轮询时间,用于等待还未返回的 I/O 事件,比如服务器的回应、用户移动鼠标等等。这个阶段的时间会比较长。如果没有其他异步任务要处理(比如到期的定时器),会一直停留在这个阶段,等待 I/O 请求返回结果。
在这个阶段会一直去问执行相关任务的模块任务执行完了没有,一旦任务执行完成,相关的数据就会放到一个回调里面然后加入到poll的队列中,也就是除了timers阶段外的所有回调都是在poll这个阶段处理的。poll阶段会一直重复检查刚才timers里没有到时间的计时器有没有到时间,如果到时间了,就直接通过check到达timers阶段通知timers执行这个回调,然后从timers队列中清除。
- Check,只处理 setImmediate 的回调函数
- Close Callback,专门处理一些 close 类型的回调,如关闭网络连接等
> **关于setTImeout和setImmediate的执行顺序**
> 答:正常情况下是setImmediate先执行,只有第一次启动nodejs的情况下timers里的定时器到时间了`才可能`setTimeout先执行。
> 原因:因为nodejs启动会执行三件事,开始执行脚本也就是执行你页面的代码,
这时候会执行你的setTImeout,然后才会去处理event loop,
而从执行脚本到处理event loop中间也需要时间,setTimeout最小的时间是4ms,
如果从执行脚本到处理event loop花了5ms时间,那么一进入timers就会发现时间到了就会立刻处理回调,
所以这时候setTimeout就会先执行
样例
setTimeout(()=>{
console.log('timeout') 3
process.nextTick(()=>{
console.log('timeout next tick') 4
})
})
setImmediate(()=>{
console.log('immediate') 5
})
console.log('cc') 1
process.nextTick(()=>{
console.log('next tick') 2
})
//cc
//next tick
//timeout
//timeout next tick
//immediate
// 第一圈事件循环,会把任务加到异步任务列表中
主线程执行栈处理
宏任务
- 主体script
- setTimeout
- setInterval
- setImmediate (Node独有)
- requestAnimationFrame (浏览器独有)
- I/O
- UI rendering (浏览器独有)
微任务 Promise.then,process.nextTick
- process.nextTick (Node独有)
- Promise
- Object.observe
- MutationObserver
执行顺序(宏任务->微任务->宏任务->微任务···)
具体怎么执行,咱们一起看两个案例分析,看完绝对就没问题了。
setTimeout(() => {
new Promise(resolve => {
// 宏任务
console.log('promise') // 1
resolve();
}).then(() => {
//微任务
console.log('then') // 3
setTimeout(() => {
console.log('then promise') // 5
}, 1000)
});
// 宏任务
console.log(1); // 2
// 宏任务
setTimeout(() => {
console.log(1) }, 1000); // 4
}, 1000);
});
##
- 首先,会执行第一个setTimeout(宏任务),把其推入eventLoop,由于没有其他的宏任务,
当时间到了时,会直接将其回调推入执行栈。
- 宏任务先执行, newPromise先执行打印promise,
- 再打印1,
- 然后再执行setTimeout,将其推入eventLoop,
- 没有宏任务了,再执行微任务.then(),打印then,
- 最后主线程执行完毕,将第二个 setTimeout执行内容从事件队列中推入主线程
- 最后打印then promise
setTimeout(function () {
console.log('setTimeout'); // 4
})
new Promise(function (resolve) {
console.log('promise'); // 1
resolve()
}).then(function () {
console.log('then'); // 3
})
console.log('console'); // 2
##
这段代码作为宏任务,进入主线程。
- 先遇到setTimeout,那么将其回调函数注册后分发到宏任务Event Queue。(注册过程与上同,下文不再描述)
- 接下来遇到了Promise,new Promise立即执行,then函数分发到微任务Event Queue。
- 遇到console.log(),立即执行。
ok,第一轮事件循环结束了,
- 微任务中去promise.then
我们开始第二轮循环,
- 当然要从宏任务Event Queue开始。我们发现了宏任务Event Queue中setTimeout对应的回调函数,立即执行。
3.6 模块化 CommonJS
模块化用来分割,组织和打包代码。每个模块完成一个特定的子功能,所有的模块按某种方法组装起来,成为一个整体, 完成整个系统所要求的功能。
定义模块
模块化特点:
- 一个文件就是一个独立的模块(模块作用域)
- 模块加载采用
同步模式 - 通过 require 函数导入、exports 对象导出
作用域:
- 一个文件就是一个独立模块
- 每个模块都有自己独立的作用域
即:
require('../2.js')导入的模块中有let a = 1
本文件中也有let a = 2
各自在各自的作用域中,不会覆盖
2.js中的a并没有影响到当前这个模块的a。
module对象
每一个模块中都会有一个内置的对象:module
该对象提供了包括当前模块文件所拥有的一些信息。
module对象
id: 当前模块的唯一标识,默认id为当前这个文件的绝对路径
filename: 当前模块的文件路径
parent
children
loaded
paths
function Module(id, parent) {
this.id = id;
this.exports = {};
this.parent = parent;
this.filename = null;
this.loaded = false;
this.children = [];
}
module.exports = Module;
var module = new Module(filename, parent);
分类
- File Modules(文件模块机制【一个文件即一个模块】)
- Folders as Modules(文件夹模块机制)
- node_modules Folders(特殊的文件夹模块)
- global folders(global开发或者上线环境公用,直接在 node安装路径下)
如果我们导入的模块是在node_modules目录下的,又会有另外的一种规则
(不以 ./ ../ / 开始的模块,按照另外一种加载机制进行加载):
* 当非路径加载模式的时候,会按照如果下规则进行模块的查找:
* 在module对象有一个属性,paths,是一个数组,里面保存的就是这种非路径加载模式需要查找的路径列表
(数组中一个个去查),子级可以向父级node_modules去查,但父级不可向子级node_modules 中去查
当我们导入的模块名称是一个文件夹的时候【文件夹模块机制】
* 1.读取该文件夹下的package.json文件
* 2.导入package.json文件中main选项指定的文件
* 3.如果不存在package.json或者main指定的文件,这默认自动导入模块文件夹下的index.js
- Core Modules(核心模块机制)
如果自己定义的模块与核心模块冲突了,那么默认加载的是核心模块。
模块导出
每一个模块文件中有一个 exports 对象,在模块对象 module下有一个属性 exports。
- exports 对象,默认是个空对象
- exports 对象时module.exports对象的引用
模块导入(依赖)
module.require()就是模块中直接用到的require()
require(模块id/路径) 返回被导入模块中的 module.exports 对象
(1)require的模块加载机制
- 1、先计算模块路径;
- 2、如果模块在缓存里面,取出缓存, 如果是核心模块,取出模块;
- 优先从缓存中加载
- 如果已经
require过,不会重复执行加载,直接可以拿到里面的接口对象. - 目的是避免重复加载,提高模块加载效率
- 如果已经
- 判断模块标识符
require('模块标识符')- 核心模块, 取出模块加载
- 优先从缓存中加载
- 3、new Module并加载模块;
- 路径加载(自定义模块)
路径加载模式: 如果模块的加载是以 ./ ../ / 开始的,那么就是路径模块加载模式 非路径加载模式: 不以 ./ ../ / 开始的模块,按照另外一种加载机制进行加载。 包括:node_modules、全局目录、核心模块;- 非路径加载(第三方模块)
- 4、输出模块的exports属性;
// require 其实内部调用 Module._load 方法
Module._load = function(request, parent, isMain) {
// 计算绝对路径
var filename = Module._resolveFilename(request, parent);
// 第一步:如果有缓存,取出缓存
var cachedModule = Module._cache[filename];
if (cachedModule) {
return cachedModule.exports;
// 第二步:是否为内置模块
if (NativeModule.exists(filename)) {
return NativeModule.require(filename);
}
/********************************这里注意了**************************/
// 第三步:生成模块实例,存入缓存
// 这里的Module就是我们上面的1.1定义的Module
var module = new Module(filename, parent);
Module._cache[filename] = module;
/********************************这里注意了**************************/
// 第四步:加载模块
// 下面的module.load实际上是Module原型上有一个方法叫Module.prototype.load
try {
module.load(filename);
hadException = false;
} finally {
if (hadException) {
delete Module._cache[filename];
}
}
// 第五步:输出模块的exports属性
return module.exports;
};
(2)加载模块时,为什么每个模块都有__dirname, __filename属性呢,new Module的时候我们看到没有这两个属性的,那么这两个属性是从哪里来的?
每个module里面都会传入__filename, __dirname参数,这两个参数并不是module本身就有的,是外界传入的。
// 上面的第四步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
}
});
3.7 Process 进程
全局对象,不需要 require;
process 对象是一个全局变量,它提供当前 Node.js 进程的有关信息,以及控制当前 Node.js 进程
API
.argv
用来获取当前运行node程序时(cmd中指令 node 2 -v )的相关参数
console.log( process.argv ); //返回的是一个数组
if (process.argv.includes('-v')) {console.log('v1.0.0'); }
.env
系统环境变量
console.log( process.env );
.exit([code])
退出当前进程
process.exit()
process.stdout
标准输出流
process.stdout.write(data[, encoding][, callback])
process.stdin
标准输入流
process.stdin.on('data', (e) => {
//e 默认是buffer,输入后按回车触发,会 有换行符
// console.log('项目名称:', e.toString());
fs.mkdirSync( e.toString().replace('\r\n', '') );
process.stdout.write('项目创建成功');
});
process.memoryUsage()
获取内存使用情况,返回如下
{
rss: 4935680,
heapTotal: 1826816,
heapUsed: 650472,
external: 49879
}
解释一下:
heapTotal 和 heapUsed 代表V8的内存使用情况。 external代表V8管理的,绑定到Javascript的C++对象的内存使用情况。 rss, 驻留集大小, 是给这个进程分配了多少物理内存(占总分配内存的一部分) 这些物理内存中包含堆,栈,和代码段。
-
process.send(message[, sendHandle[, options]][, callback])
如果使用 IPC 通道衍生 Node.js,则可以使用process.send()方法向父进程发送消息。 消息将作为父对象ChildProcess对象上的'message'事件接收。
如果 Node.js 没有使用 IPC 通道衍生,则process.send将是undefined。 -
.on('message', handler)
如果 Node.js 进程是使用 IPC 通道衍生(参见子进程和集群文档),则每当子进程收到父进程使用childprocess.send()发送的消息时,就会触发'message'事件。
3.8 Path 路径操作
```
const path = require('path');
```
- path.basename()
获取路径的最后一部分 quux.html
// console.log(path.basename('/foo/bar/baz/asdf/quux.html')); - path.dirname()
获取路径
// console.log(__dirname);获取路径(当前文件的路径) // console.log(path.dirname('/abc/qqq/www/abc.txt')); // 获取的路径会把 abc.txt去掉 ,只留 下 路径 - path.extname() 获取扩展名称
// console.log(path.extname('index.html'));
// path.extname('index.') 返回的是一个点.
// path.extname('index') 返回的是空的
// path.extname('.index') 返回的是空的
-
路径的格式化处理
- path.format() obj->string
- path.parse() string->obj
-
path.isAbsolute() 判断是否为绝对路径
// console.log(path.isAbsolute('C:/foo/..')); // console.log(path.isAbsolute('C:\foo\..')) -
path.join() 拼接路径(最后的两个点 ..表示上层路径;.表示当前路径),在连接路径的时候会格式化路径,
// console.log(path.join('/foo', 'bar', 'baz/asdf', 'quux', '../../')); 两层..故路径只到 baz -
path.normalize() 规范化路径
// console.log(path.normalize('C:\temp\\foo\bar\..\')); 路径到 foo -
path.relative() 计算相对路径 从1路径到2路径的相对路径\
// console.log(path.relative('C:\orandea\test\aaa', 'C:\orandea\impl\bbb')); -
path.resolve() 解析路径
// console.log(path.resolve('wwwroot', 'static_files/png/', '../gif/image.gif')); 先进入 从当前路径 先进入 wwwroot路径 /home/myself/node/wwwroot 再往下走 /home/myself/node/wwwroot/static_files/png/ 再往下走 /home/myself/node/wwwroot/static_files/gif/img.gif
3.9 Buffer基本操作
Buffer 库为 Node.js 带来了一种存储原始数据的方法,可以让 Node.js 处理二进制数据,每当需要在 Node.js 中处理I/O操作中移动的数据时,就有可能使用 Buffer 库。原始数据存储在 Buffer 类的实例中。一个 Buffer 类似于一个整数数组,但它对应于 V8 堆内存之外的一块原始内存。
Buffer对象是Node处理8位二进制数据的一个接口(0-255)。
Buffer 类在 Node.js 中是一个全局变量
本质:
字节数组,操作字节, 长度固定
新建Buffer会占用V8分配的内存吗?
不会,Buffer属于堆外内存,不是V8分配的。
3.9.1 实例化
buffer需要先指定字节长度
new Buffer()初始化实例的时候可以传入array、size、string,
但是当new Buffer(size)时初始化存在问题会随机初始化(不一定为零), buffer.alloc() 初始化的都为零 ,encoding默认 为 'utf8',所以推荐。
new Buffer(array | size | string)Buffer.from(array)Buffer.from(string)//可以追加第二个参数 Buffer.from('hello','utf8') 编码方式Buffer.alloc(size)
3.9.2 功能方法
Buffer.isEncoding()判断是否支持该编码 Buffer.isBuffer()判断是否为Buffer //参数是一个对象Buffer.byteLength()返回指定编码的字节长度,默认utf8
let buf = Buffer.from('中国','ascii');
console.log(Buffer.byteLength(buf));
Buffer.concat()将一组Buffer对象合并为一个Buffer对象 参数为一个列表
let buf3 = Buffer.concat( [buf1,buf2] );
3.9.3 实例方法
write()向buffer对象中写入内容
let buf = Buffer.alloc(5);
// 结果 buffer 00 00 00 00 00
buf.write('hello',2,2);
// 结果buffer0000 686500
第二个参数:开始位置索引 从第几个位置开始写,若buffer有长度限制,多余的部分则不写进去
第三个参数: 长度 写入几个字节
.slice()截取新的buffer对象
let buf1 = buf.slice(2,3); 半开半闭区间 [2,3)从第二个位置开始截取 但不包含第三个位置的元素
!!!!!!修改slice返回的切片buffer也会影响到原来的buffer
-
.toString()把buf对象转成字符串 -
.toJson()把buf对象转成json形式的字符串。
toJSON方法不需要显式调用,当JSON.stringify(buf)方法调用的时候会自动调用toJSON方法 返回结果类型:{“ type”:“Buffer”,“data”:[104,101,108,108,111]} //对应hello十进制编码 -
buf[index]Buffer也有下标,可以通过 buf[index] 进行操作,此方 法赋值除了数字外(如:字符串)都需要先转成二进制数据(否则 操作无效赋值00),若下标越界(size)操作无效. -
.copy()
buf.copy(target[, targetStart[, sourceStart[,sourceEnd]]])
!!!!修改拷贝copy后的buffer不会影响被拷贝的原buffer
3.10 Stream
流(stream)是一种在 Node.js 中处理流式数据的抽象 接口。 stream 模块提供了一些基础的 API,用于构建实现了流 接口的对象,Node.js 中许多的对象都是提供了流的实现:fs文 件操作、net、dgram、http、https等。
流是基于事件的 API,用于管理和处理数据。
理解流的最好方式就是想象一下没有流的时候怎么处理数据:
fs.readFileSync同步读取文件,程序会阻塞,所有数据被读到内存fs.readFile阻止程序阻塞,但仍会将文件所有数据读取到内存中- 希望少内存读取大文件,读取一个数据块到内存处理完再去索取更多的数据
require('stream')
流的基本类型
Writable- 可写入数据的流(例如fs.createWriteStream())Readable- 可读取数据的流(例如fs.createReadStream())Duplex- 可读又可写的流(例如 net.Socket)Transform- 在读写过程中可以修改或转换数据的 Duplex 流(例如zlib.createDeflate())
Writable属性方法
- .write(chunk[, encoding][, callback])
- .end([chunk][, encoding][, callback])
- .setDefaultEncoding(encoding)
Readable属性方法
- .setEncoding(encoding)
- .read([size])
- .pipe(destination[, options])
- .pause()
- .resume()//恢复
流的错误处理
stream.on('error', (err) => {
console.trace();
console.error('Stack:', err.stack);
console.error('The error raised was:', err);
});
3.11 FileSystem文件系统
文件信息获取
const fs = require('fs');
- .stat(path, (err, data)=>{})
fs.stat('./abc',(err,statE) => {\
// 一般回调函数的第一个参数是错误对象,
//如果err为null,表示没有错误,否则表示报错了
if(err) return;
})
- statE的属性
```
atime 文件访问时间
ctime 文件的状态信息发生变化的时间(比如文件的权限)
mtime 文件数据发生变化的时间
birthtime 文件创建的时间
```
- statE.isFile()
判断是否为文件
- statE.isDirectory()
判断是否为目录
fs.readFile() & readFileSync() 读文件操作
- 如果有第二个参数,且其是编码,那么回调函数获取到的数据就是字符串;
- 编码指定 从文件中 读取出来 后的 数据格式;
- 如果没有第二个参数,那么得到的就是Buffer实例对象
const fs = require('fs'); const path = require('path');
let strpath = path.join(__dirname,'data.txt');
fs.readFile(strpath, (err,data) =>{
if(err) return;
console.log(data.toString());
})
fs.readFile(strpath,'utf8',(err,data)=>{
if(err) return;
//console.log(data.toString());
console.log(data); // Buffer
});
// 同步操作
//let ret = fs.readFileSync(strpath,'utf8');
//console.log(ret);
fs.writeFile() & writeFileSync()写文件操作
- 编码方式字段同读操作,不过此处为第三个参数,因为第二个参数为要写的内容。
- 第三个参数指定编码 ,指定写入到文件中时的编码方式。
- 如果指定utf-8,不管第二个参数 是字符串还是 buffer 对象 写入到文件中的 都是 utf8编码过的。
const fs = require('fs'); const path = require('path');
let strpath = path.join(__dirname,'data.txt');
fs.writeFile(strpath,'hello nihao','utf8',(err)=>{
if(!err){
console.log('文件写入成功');
}
});
let buf = Buffer.from('hi');
fs.writeFile(strpath,buf,'utf8',(err)=>{
if(!err){
console.log('文件写入成功');
}
});
// 同步操作
//fs.writeFileSync(strpath,'tom and jerry');
如果文件不存在,则创建,若文件存在 ,全部内容完全覆盖;
如果目录(路径)不存在,创建文件就会失败;
fs.appendFile()
文件尾部追加内容 fs.appendFile( )不会覆盖原来内容
fs.existsSync(appRoot)
该路径文件夹或文件是否存在,true/false
大文件操作(流式操作)
- fs.createReadStream(path[, options]) //读取文件的事件流
- fs.createWriteStream(path[, options]) //写文件的事件流
const path = require('path');
const fs = require('fs');
let spath = path.join(__dirname,'../03source','file.zip');\
let dpath = path.join('C:\Users\www\Desktop','file.zip'); //目标路径
let readStream = fs.createReadStream(spath);
let writeStream = fs.createWriteStream(dpath);
// 基于事件的处理方式
let num = 1;
readStream.on("data", (chunk) => {
num++;
writeStream.write(chunk)
})
// data事件在文件中数据读取一部分的时候就执行
readStream.on('end',()=>{
console.log('文件处理完成'+num);
});
- .pipe() pipe的作用直接把输入流和输出流连到一起,把读取到的数据流直接写到写数据流的入口.
readStream.pipe(writeStream);
const fs = require('fs');
http.createServer((req, res) => {
fs.createReadStream(`${__dirname}/index.html`).pipe(res);
}).listen(8000);
const readable = fs.createReadStream('./original.txt');
const writeable = fs.createWriteStream('./copy.txt');
readable.pipe(writeable);
fs.FSWatcher类,监控文件
监控文件
fs.watch(filename[, options][, listener])
fs.watchFile(filename[, options], listener)
fs.watchFile 比 fs.watch 低效,但更好用。
// 当文件发生改变的时候,触发回调
fs.watchFile('./data.txt', info=> {
//info : 保存当前变化的细节信息stats
console.log(info);
})
// 监听文件或目录\
fs.watch('./a', (eventType, filename) => {
// eventType: change或者rename 增加或者删除文件触发rename
// filename: 当前发生改变的具体文件
console.log(eventType, filename);
});
目录操作
1、创建目录
fs.mkdir(path[, mode], callback) fs.mkdirSync(path[, mode])
// fs.mkdirSync(path.join(__dirname,'hello')) 在当前目录下 加一级hello目录(即一个hello文件 夹)
2、读取目录
fs.readdir(path[, options], callback)
fs.readdirSync(path[, options])
3、删除目录
fs.rmdir(path, callback) 要删除的 fs.rmdirSync(path)
其他操作
-
fs.rename() 文件重命名
-
fs.unlink() 删除文件
3.12 Events(核心模块)
事件是整个 Node.js 的核心,Node.js中大部分模块都使用或继承了该模块(类似 WebAPI 中的EventTarget)。
require('events')
EventEmitter 类const EventEmmiter = require('events');
- .
emit(eventName[, ...args]) //触发事件 - .
addListener(eventName, listener) //注册事件(后加) - .
prependListener(eventName, listener)(前加) - .
on(eventName, listener) - .
off(eventName, listener) - .
removeListener(eventName, listener) - .eventNames()//返回一个已注册事件的数组
- .setMaxListeners(num)//每个事件的最大注册次数 超出num会报出警告
3.13 V8垃圾回收机制
3.13.1 如何查看V8的内存使用情况
使用process.memoryUsage(),返回如下
heapTotal 和 heapUsed 代表V8的内存使用情况。 external代表V8管理的,绑定到Javascript的C++对象的内存使用情况。 rss, 驻留集大小, 是给这个进程分配了多少物理内存(占总分配内存的一部分) 这些物理内存中包含堆,栈,和代码段。
3.13.2 V8的内存限制是多少,为什么V8这样设计
64位系统下是1.4GB, 32位系统下是0.7GB。因为1.5GB的垃圾回收堆内存,V8需要花费50毫秒以上,做一次非增量式的垃圾回收甚至要1秒以上。这是垃圾回收中引起Javascript线程暂停执行的事件,在这样的花销下,应用的性能和影响力都会直线下降。
3.13.3 V8的内存分代 和 回收算法
在V8中,主要将堆内存分为新生代和老生代两代。新生代中的对象存活时间较短的对象,老生代中的对象存活时间较长,或常驻内存的对象。
分代的唯一理由就是优化GC性能
- Minor GC 会清理年轻代的内存。
- Major GC 是清理老年代。
- Full GC 是清理整个堆空间—包括年轻代和老年代。
3.13.3.1 新生代
新生代中的对象主要通过Scavenge算法进行垃圾回收。这是一种采用复制的方式实现的垃圾回收算法。它将堆内存一份为二,每一部分空间成为semispace。在这两个semispace空间中,只有一个处于使用中,另一个处于闲置状态。处于使用状态的semispace空间称为From空间,处于闲置状态的空间称为To空间。
- 当开始垃圾回收的时候,会先检查From空间中的存活对象,这些
存活对象将被复制到To空间中,而非存活对象占用的空间将会被释放。完成复制后,From空间和To空间发生角色对换。 - 应为新生代中对象的生命周期比较短,就比较适合这个算法。
- 当一个对象经过
多次复制依然存活,它将会被认为是生命周期较长的对象。这种新生代中生命周期较长的对象在复制过程中会被移到老生代中,再对From和To进行对换。
新生代对象晋升到老生代有两个条件:
(1)第一个是判断是对象否已经经过一次 Scavenge 回收。
若经历过,则将对象从 From 空间复制到老生代中;若没有经历,则复制到 To 空间。
(2)第二个是 To 空间的内存使用占比是否超过限制。
当对象从 From 空间复制到 To 空间时,若 To 空间使用超过 25%,则对象直接晋升到老生代中。
设置 25% 的原因主要是因为算法结束后,两个空间结束后会交换位置,
如果 To 空间的内存太小,会影响后续的内存分配。
更细维度
新生代又分为了三部分:1个Eden区和2个Survivor区(分别叫from和to)。默认比例为8:1:1。
为什么要设置Survivor区? Survivor的存在意义,就是减少被送到老年代的对象,进而减少Full GC的发生,Survivor的预筛选保证,只有经历16次Minor GC还能在新生代中存活的对象,才会被送到老年代。
一般情况下, 新创建的对象都会被分配到Eden区(一些大对象特殊处理),这些对象经过第一次Minor GC后,如果仍然存活,将会被移到Survivor区。对象在Survivor区中每熬过一次Minor GC,年龄就会增加1岁,当它的年龄增加到一定程度时,就会被移动到年老代中。
在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。
那么,顺理成章的,应该建立两块Survivor区,刚刚新建的对象在Eden中,经历一次Minor GC,Eden中的存活对象就会被移动到第一块survivor space S0,Eden被清空;等Eden区再满了,就再触发一次Minor GC,Eden和S0中的存活对象又会被复制送入第二块survivor space S1(这个过程非常重要,因为这种复制算法保证了S1中来自S0和Eden两部分的存活对象占用连续的内存空间,避免了碎片化的发生)。S0和Eden被清空,然后下一轮S0与S1交换角色,如此循环往复。如果对象的复制次数达到16次,该对象就会被送到老年代中。
3.13.3.2 老生代
老生代主要采取的是标记清除的垃圾回收算法。与Scavenge复制活着的对象不同,标记清除算法在标记阶段遍历堆中的所有对象,并标记活着的对象,只清理死亡对象。活对象在新生代中只占叫小部分,死对象在老生代中只占较小部分,这是为什么采用标记清除算法的原因。
3.13.3.3 标记清除算法的问题
主要问题是每一次进行标记清除回收后,内存空间会出现不连续的状态
- 这种内存碎片会
对后续内存分配造成问题,很可能出现需要分配一个大对象的情况,这时所有的碎片空间都无法完成此次分配,就会提前触发垃圾回收,而这次回收是不必要的。即使放得下,也可能存在GC效率的问题。 - 为了解决碎片问题,标记整理被提出来。就是在对象被标记死亡后,在整理的过程中,
将活着的对象往一端移动,移动完成后,直接清理掉边界外的内存。
3.13.4 V8 GC的触发
在网上找了一张比较容易理解的图(来自知乎@夏木)
为了确保js的正常执行,gc和js执行不能并行执行,因此选择了类似于共享CPU时间片的执行方式,将gc 和js交替执行。
3.13.5 哪些情况会造成V8无法立即回收内存
闭包和全局变量
3.13.6 请谈一下内存泄漏是什么,以及常见内存泄漏的原因,和排查的方法
什么是内存泄漏
-
内存泄漏(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),将造成内存泄漏。这种情况很容易在复用对象上添加事件时出现,所以事件重复监听可能收到如下警告:
emitter.setMaxListeners() to increase limit例如,Node.js 中 Agent 的 keepAlive 为 true 时,可能造成的内存泄漏。当 Agent keepAlive 为 true 的时候,将会复用之前使用过的 socket,如果在 socket 上添加事件监听,忘记清除的话,因为 socket 的复用,将导致事件重复监听从而产生内存泄漏。
原理上与前一个添加事件监听的时候忘了清除是一样的。在使用 Node.js 的 http 模块时,不通过 keepAlive 复用是没有问题的,复用了以后就会可能产生内存泄漏。所以,你需要了解添加事件监听的对象的生命周期,并注意自行移除。
-
排查方法
想要定位内存泄漏,通常会有两种情况:
- 对于只要正常使用就可以重现的内存泄漏,这是很简单的情况只要在测试环境模拟就可以排查了。
- 对于偶然的内存泄漏,一般会与特殊的输入有关系。想稳定重现这种输入是很耗时的过程。如果不能通过代码的日志定位到这个特殊的输入,那么推荐去生产环境打印内存快照了。
- 需要注意的是,打印内存快照是很耗 CPU 的操作,可能会对线上业务造成影响。 快照工具推荐使用 heapdump 用来保存内存快照,使用 devtool 来查看内存快照。
- 使用 heapdump 保存内存快照时,只会有 Node.js 环境中的对象,不会受到干扰(如果使用 node-inspector 的话,快照中会有前端的变量干扰)。
- PS:安装 heapdump 在某些 Node.js 版本上可能出错,建议使用 npm install heapdump -target=Node.js 版本来安装。