# Nodejs初探

1、node事件循环 (opens new window)

2、V8 引擎源码 (opens new window)

3、node源码 (opens new window)

# 1、简介

nodejs是一个基于chrome V8引擎的js运行环境,使用了一个事件驱动、非阻塞式i/o的模型,使其轻量又高效。

# 1、安装

  • 通过node.js 官⽹的⼆进制安装⽂件来安装
  • 通过二进制编译方式来安装,打包上线的一些nodejs环境,eg:docker、linux不适合通过安装文件安装

# 2、版本切换

使用nvm和n工具:

nvm 的全称就是 node version manager,意思就是能够管理 node 版本的⼀个⼯具,它提供了⼀种直接通过shell 执⾏的⽅式来进⾏安装,原理:通过nvm切换环境变量里node命令指向的软件路径。

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash
1

# 3、周边工具解析

  • 性能优化:压力测试ab、webbench
  • 内存泄露于debug:vsc工具或断点
    • vscode加断点调试:run and debug
    • node工具和插件
      • node --inspect --inspect-brk xxx:生成websocket链接
      • chrome插件:NIM的inspect进行调试和内存快照查看,输入chrome://inspect/#devices
  • 轻量的js解析引擎:quickjss
  • 服务端js运行环境:deno
    • 基于 rust 和 typescript 开发⼀些上层模块
    • ⽀持从 url 加载模块,同时⽀持 top level await 等特性

参考资料:nodejs调试 (opens new window)

# 2、commonjs模块

# 1、用法

  • 引入模块:require("./index.js")
  • 导出模块:module.exports={...} 或者 exports.key={}
    • 注意:exports为module.exports的引用,不可使用exports直接赋值,模块无法导出,eg:exports={}
  • 缓存:require值会缓存,通过require引用文件时,会将文件执行一遍后,将结果通过浅克隆的方式,写入全局内存,后续require该路径,直接从内存获取,无需重新执行文件
  • 值拷贝:模块输出是值的拷贝,一但输出,模块内部变化后,无法影响之前的引用,而ESModule是引用拷贝。commonJS运行时加载,ESModule编译阶段引用
    • CommonJS在引入时是加载整个模块,生成一个对象,然后再从这个生成的对象上读取方法和属性
    • ESModule不是对象,而是通过export暴露出要输出的代码块,在import时使用静态命令的方法引用制定的输出代码块,并在import语句处执行这个要输出的代码,而不是直接加载整个模块

TIP

面试题:为何直接使用exports导出commonjs模块,eg:exports = “hello”

解析:commonjs模块是通过module.exports导出模块,exports为module.exports的引用,使用exports直接赋值导出模块,只会改变exports的引用不会导出commonjs模块。

// cjs正确导出用法
exports.key = 'hello world'
module.exports = "hello world"
//错误导出
exports = "hello world" //无法输出

//解析:
const obj = {
  key: {}
}
obj.key = "hello world"  //可改变obj
const key = obj.key
key.key1 = "hello world" //可改变obj 
key = "hello world"      //无法改变obj,改变了key的引用
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 2、原理实现

使用fs、vm、path内置模块,以及函数包裹形式实现

const vm = require("vm")
const path = require("path")
const fs = require("fs")

/**
 * commonjs的require函数:引入module
 * @param {string} filename module的名称
 */
function customRequire(filename){
    const pathToFile = path.resolve(__dirname, filename)
    const content = fs.readFileSync(pathToFile, 'utf-8')
    //使用函数包裹模块,执行函数
    //注入形参:require、module、exports、__dirname、__filename
    const wrapper = [
        '(function(require, module, exports, __dirname, __filename){',
        '})'
    ]
    const wrappedContent = wrapper[0] + content + wrapper[1]
    const script = new vm.Script(wrappedContent, {
        filename: 'index.js'
    })
    const module = {
        exports: {}
    }
    //转换为函数,类似eval,(funcion(require, module, exports){ xxx })
    const result = script.runInThisContext();
    //函数执行,引入模块,若内部有require继续递归
    //exports为module.exports的引用
    result(customRequire, module, module.exports);
    return module.exports
}

global.customRequire = customRequire
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

# 3、源码解析

  • 源码路径:/lib/internal/modules/cjs/
//loader.js
//require函数定义
1、Module.prototype.require:调用__load函数
2、Module.__load:_cache处理,调用load函数
3、Module.prototype.load函数:调用Module._extensions[extension](this, filename);
//不同的后缀通过定义不同的函数指定解析规则:以Module._extensions['.js']为例
4、Module._extensions['.js'] = function(module, filename) {
  //读取缓存或者通过readFileSync读取内容
  if (cached?.source) {
    content = cached.source;
    cached.source = undefined;
  } else {
    content = fs.readFileSync(filename, 'utf8');
  }
  //...
  //调用compile解析
  module._compile(content, filename);
}
5、Module.prototype._compile = function(content, filename){
  //生成包裹函数:warpSafe获取函数字符串并使用vm执行生成执行函数
  const compiledWrapper = wrapSafe(filename, content, this);
  //执行函数
  const exports = this.exports;
  const thisValue = exports;
  const module = this;
  if (inspectorWrapper) {
    result = inspectorWrapper(compiledWrapper, thisValue, exports,
                              require, module, filename, dirname);
  } else {
    //静态方法Reflect.apply(target, thisArgument, argumentsList)
    //通过指定的参数列表发起对目标(target)函数的调用
    result = ReflectApply(compiledWrapper, thisValue,
                          [exports, require, module, filename, dirname]);
  }
  return result;
}
6function wrapSafe(filename, content, cjsModuleInstance) {
    /* 生成包裹函数字符:
    let wrap = function(script) {
       return Module.wrapper[0] + script + Module.wrapper[1];
    };
    const wrapper = [
      '(function (exports, require, module, __filename, __dirname) { ',
      '\n});',
    ];*/
  	const wrapper = Module.wrap(content);
    //获取包裹函数
    return vm.runInThisContext(wrapper, {
      filename,
      lineOffset: 0,
      displayErrors: true,
      importModuleDynamically: async (specifier) => {
        const loader = asyncESM.ESMLoader;
        return loader.import(specifier, normalizeReferrerURL(filename));
      },
    });
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
  • 利用源码扩展工具
    • 后缀名扩展解析
    • 切面编程
const Module = require('module')
//后缀解析扩展:.test后缀同.js后缀
Module._extensions['.test'] = Module._extensions['.js']

//切面编程:解析js模块前做打印处理
const prevFunc = Module._extensions['.js']
Module._extensions['.js'] = function(...args){ 
  console.log('load script')
  prevFunc.apply(prevFunc, args)
}
1
2
3
4
5
6
7
8
9
10

# 3、事件循环

参考资料:node事件循环6个阶段 (opens new window)node事件循环面试

  • 同步代码
  • process.nextTick和promise.then() ,之后进入事件循环
  • timers 阶段:这个阶段执行timer(setTimeout、setInterval)的回调
  • I/O callbacks 阶段 :处理一些上一轮循环中的少数未执行的 I/O 回调
  • idle, prepare 阶段 :仅node内部使用
  • poll 阶段 :获取新的I/O事件, 适当的条件下node将阻塞在这里
  • check 阶段 :执行 setImmediate() 的回调
  • close callbacks 阶段:执行 socket 的 close 事件回调

习题解析:

  • new Promise(()=>{//同步执行}).then(()=>{//异步执行})
  • async function test(){console.log() //同步 -> await test(0) //同步 -> console.log()//异步}
//习题1:
// 顺序不确定,只有两个语句,执行环境有差异
// 场景1: setTimeout 0 最少1ms,未推入timers队列中,执行结果为:setImmediate、setTimeout
// 场景2: setTimeout 0 已推入timers队列中,执行结果为:setTimeout、setImmediate
setTimeout(()=>{
  console.log('setTimeout')
}, 0)
setImmediate(()=>{
  console.log('setImmediate')
})

//习题2: 都在回调函数中,内容确定
//首轮事件循环setTimeout1的timers清空,执行至check阶段,先输出setImmediate
//第二轮事件循环setTimeout2
//最终输出:setTimeout1、setImmediate、setTimeout2
setTimeout(()=>{
  setTimeout(()=>{
  		console.log('setTimeout2')
  }, 0)
  setImmediate(()=>{
    	console.log('setImmediate')
  })
  console.log('setTimeout1')
}, 0)

//习题3: 混合题
setTimeout(()=>{
  console.log('timeout')
}, 0)
setImmediate(()=>{
  console.log('immediate')
  setImmediate(()=>{
  	console.log('immediate1')
	})
  new Promise(resolve => {
    console.log(77)
    resolve()
  }).then(()=>{
    console.log(88)
  })
  process.nextTick(function(){
    console.log('nextTick1')
  });
})
new Promise(resolve => {
  console.log(7)
  resolve()
}).then(()=>{
  console.log(8)
})
process.nextTick(function(){
  console.log('nextTick2')
})
console.log('start')

// 第一轮:7 start -  timeout | immediate 77 | 8 | nextTick2   
// 第二轮:7 start nextTick2  8 timeout immediate 77 - immediate1 | 88 | nextTick1
// 第三轮:7 start nextTick2  8 timeout immediate 77 nextTick1 88 immediate1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

# 4、模块

# 1、底层依赖模块

  • V8 引擎:主要是 JS 语法的解析,有了它才能识别 JS语法

  • libuv:C 语⾔实现的⼀个⾼性能异步⾮阻塞 IO 库,⽤来实现 node.js 的事件循环

  • http-parser/llhttp:底层处理 http 请求,处理报⽂,解析请求包等内容

  • openssl:处理加密算法,各种框架运⽤⼴泛,底层依赖,无需js实现

  • zlib:处理压缩等内容

# 2、全局对象

  • 全局对象:global,下挂如下对象和函数,使用时无需模块引入
    • global
    • Buffer、process、console、queueMicrotask
    • setTimeout、clearTimeout、setInterval、setImmediate、clearInterval
  • 模块中使用注入变量:
    • __filename:当前文件名称带路径,eg:/Users/jian/workspace/cjs/index.js
    • __dirname:当前文件夹名称,eg:/Users/jian/workspace/cjs/
    • cjs:require, module, exports
//全局this指向global
//模块文件中this指向:module.exports的对象
/* 模块中才可以使用的变量:commonjs模块注入,非模块中使用报错
__dirname、__filename、exports、module、require */

//node命令行中
> console.log(__dirname)
// Uncaught ReferenceError: __dirname is not defined
> console.log(this)
/* <ref *1> Object [global] {
  global: [Circular *1],
  clearInterval: [Function: clearInterval],
  clearTimeout: [Function: clearTimeout],
  setInterval: [Function: setInterval],
  setTimeout: [Function: setTimeout] {
    [Symbol(nodejs.util.promisify.custom)]: [Getter]
  },
  queueMicrotask: [Function: queueMicrotask],
  clearImmediate: [Function: clearImmediate],
  setImmediate: [Function: setImmediate] {
    [Symbol(nodejs.util.promisify.custom)]: [Getter]
  }
}*/

//node模块中执行
console.log(this)
console.log(__filename)
console.log(__dirname)
/* 
{}
/Users/jian/workspace/cjs/index.js
/Users/jian/workspace/cjs
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

# 1、process

  • argv:返回一个数组,由命令行执行脚本时的各个参数组成。它的第一个成员总是node,第二个成员是脚本文件名,其余成员是脚本文件的参数
  • env:返回一个对象,成员为当前 shell 的环境变量
  • stdin:标准输入流
  • stdout:标准输出流
  • cwd():返回当前进程的工作目录
console.log(process.argv)
//[ '/usr/local/bin/node', '/Users/jian/workspace/cjs/index.js' ]
console.log(process.cwd()) ////Users/jian/workspace/cjs
process.stdout.write("Hello World!"); //Hello World!
1
2
3
4

# 3、常见内置模块

node.js 中最主要的内容,就是实现了⼀套 CommonJS的模块化规范,以及内置了⼀些常⻅的模块。

  • fs:⽂件系统,能够读取写⼊当前安装系统环境中硬盘的数据
  • path:路径系统,能够处理路径之间的问题
  • crypto:加密相关模块,能够以标准的加密⽅式对我们的内容进⾏加解密
  • dns:处理 dns 相关内容,例如我们可以设置 dns 服务器等等
  • http: 设置⼀个 http 服务器,发送 http 请求,监听响应等等
  • readline: 读取 stdin 的⼀⾏内容,可以读取、增加、删除我们命令⾏中的内容
  • os:操作系统层⾯的⼀些 api,例如告诉你当前系统类型及⼀些参数
  • vm: ⼀个专⻔处理沙箱的虚拟机模块,底层主要来调⽤ v8 相关 api 进⾏代码解析

# 1、path模块

/*
1、path.join([path1][, path2][, ...]):连接路径,主要用途在于,会正确使用当前系统的路径分隔符
2、path.resolve([from ...], to):用于返回绝对路径,类似于多次cd处理
3、path.dirname(p):返回文件夹
4、path.extname(p):返回后缀名
5、path.basename(p):返回路径中的最后一部分
*/

const path = require("path");

// 连接路径:/test/test1/2slashes/1slash
console.log('joint path : ' + path.join('/test', 'test1', '2slashes/1slash', 'tab', '..'));

// 转换为绝对路径:/Users/jian/workspace/cjs/main.js
const mainPath = path.resolve('main.js')
console.log('resolve : ' + mainPath);

// 路径中文件的后缀名:.js
console.log('ext name : ' + path.extname(mainPath));

// 路径最后名字:main.js
console.log('ext name : ' + path.basename(mainPath));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 2、fs

参考资料:Node.js 高级进阶之 fs 文件模块学习 (opens new window)

  • 文件常识

    • 权限位mode:文件所有者/文件所属组/其他用户的rwx,eg:默认0o666,对应十进制438
    • 标志位flag:文件的操作方式,r、w、s同步、+增加相反操作、x排他方式
    • 文件描述符fd:识别和追踪文件
  • 完整性读写文件操作

    • fs.readFile(filename,[encoding],[callback(error,data)]
    • fs.writeFile(filename,data,[options],callback)
      • options:可选,为对象,{encoding, mode, flag}
      • 默认编码为 utf8,模式为 0666(可读可写可操作),flag 为 'w'
    • fs.unlink(filename, callback)
  • 指定位置读写文件操作(高级文件操作)

    • fs.open(path,flags,[mode],callback)
    • fs.read(fd, buffer, offset, length, position, callback);
    • fs.write(fd, buffer, offset, length, position, callback);
    • fs.close(fd,callback)
//nodejs中最好使用绝对路径
const pathToFile = path.resolve(__dirname, './text')
//err first, 异步
fs.readFile(pathTofile, 'utf-8', function(err, result){
  if(err){
    console.log('e', e)
    return err
  }
  console.log('result', result)
})
//同步文件
fs.readFileSync(pathToFile, 'utf-8')

//耦合promise封装
function read(path) {
  return new Promise(function (resolve, reject) {
    fs.readFile(path, { flag: 'r', encoding: 'utf-8' }, function (err, data) {
      if (err) {
        // 失败执行的内容
        reject(err)
      } else {
        // 成功执行的内容
        resolve(data)
      }
    })
  })
}

//通用promise封装,高版本已内置该库
function promisify(func){
  return function(...args){
    return new Promise((resolve, reject)=>{
      args.push(function(err, result){
        if(err) return reject(err)
        return resolve(result)
      })
      return func.apply(func, args)
      /* 等价于
      fs.readFile(path, { flag: 'r', encoding: 'utf-8' }, function (err, data) {
      	if (err) {
        	// 失败执行的内容
        	reject(err)
      	} else {
        	// 成功执行的内容
        	resolve(data)
      	}
    	}) */
    })
  }
}
const creadFileAsync = promisify(fs.readFile)
creadFileAsync(pathToFile, 'utf-8')
	.then(content => {
  	console.log(content)
	}).catch(e=>{
  	console.log('e', e)
	})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

# 3、http

var http = require('http');
var fs = require('fs');
var url = require('url');
 
// 创建服务器
http.createServer( function (request, response) {  
   // 解析请求,包括文件名
   var pathname = url.parse(request.url).pathname;
   
   // 输出请求的文件名
   console.log("Request for " + pathname + " received.");
   
   // 从文件系统中读取请求的文件内容
   fs.readFile(pathname.substr(1), function (err, data) {
      if (err) {
         console.log(err);
         // HTTP 状态码: 404 : NOT FOUND
         // Content Type: text/html
         response.writeHead(404, {'Content-Type': 'text/html'});
      }else{             
         // HTTP 状态码: 200 : OK
         // Content Type: text/html
         response.writeHead(200, {'Content-Type': 'text/html'});    
         
         // 响应文件内容
         response.write(data.toString());        
      }
      //  发送响应数据
      response.end();
   });   
}).listen(8080);
 
// 控制台会输出以下信息
console.log('Server running at http://127.0.0.1:8080/');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

# 4、buffer

参考资料:buffer乱码问题 (opens new window)buffer模块解析 (opens new window)

# 5、stream

参考资料:stream模块解析 (opens new window)

# 6、EventEmitter

  • 大多数时候我们不会直接使用 EventEmitter,而是在对象中继承它,包括 fs、net、 http 在内的,只要是支持事件响应的核心模块都是 EventEmitter 的子类。

  • EventEmitter 会按照监听器注册的顺序同步地调用所有监听器,所以必须确保事件的排序正确,且避免竞态条件。

  • 常见api

    • addListener(event, listener)添加监听器
    • removeListener(event, listener)移除监听器
    • removeAllListeners([event])移除所有监听器
    • on(event, listener)注册监听器
    • off(eventName, listener)移除监听器
    • once(event, listener)为指定事件注册一个单次监听器
    • emit(event, [arg1], [arg2], [...])按监听器的顺序执行执行每个监听器,如果事件有注册监听返回 true,否则返回 false
  • 错误处理:当 EventEmitter 实例出错时,应该触发 'error' 事件。 这些在 Node 中被视为特殊情况。如果没有为 'error' 事件注册监听器,则当 'error' 事件触发时,会抛出错误、打印堆栈跟踪、并退出 Node.js 进程。作为最佳,应该始终为 'error' 事件注册监听器。

const EventEmitter = require('events').EventEmitter;
var eventEmitter = new EventEmitter();
eventEmitter.on('event', function(a, b) {
  console.log(a, b, this, this === eventEmitter);
  // 打印: 普通函数,this指向eventEmitter实例
  //   a b MyEmitter {
  //     domain: null,
  //     _events: { event: [Function] },
  //     _eventsCount: 1,
  //     _maxListeners: undefined } true
});
eventEmitter.emit('event', 'a', 'b');

eventEmitter.on('event', (a, b) => {
  //箭头函数,this指向module.exports
  console.log(a, b, this, this === module.exports);
  // 打印: a b {} true
});
eventEmitter.emit('event', 'a', 'b');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

TIP

面试题:事件模型设计(使用:发布订阅模式)

class EventEmitter{
  constructor(maxListeners){
    this.events = {}
    this.maxListeners = maxListners || Infinity
  }
  
  on(event, listener){
    if(!this.events[event]){
      this.events[event] = []
    }
    if(this.maxListener != Infinity 
       && this.events[event].length > this.maxListeners){
      console.warn(`${event} has reached max listeners.`)
      return this
    }
    this.events[event].push(listener)
    return this
  }
  
  off(event, listener){
    if(!listener){
      this.events[event] = null //无listener全部移除
    }else{
      this.events[event] = this.events[event].filter(item => item !== listener)
    }
    return this//链式调用
  }
  
  once(event, listener){
    const func = (...args) => {
      this.off(event, listener)
      listener.apply(this, args)
    }
    this.on(event, func)
  }
  
  emit(event, ...args){
    const listeners = this.events[event]
    if(!listeners){
      console.log("no listeners")
      return this
    }
    listeners.forEach(cb => {
      cb.apply(this, args)
    })
    return this
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
Last Updated: 8/15/2021, 9:38:20 PM