(一)Nodejs 初探

671 阅读8分钟

参考资料:

1、node事件循环

2、V8 引擎源码

3、node源码

一、背景

1、定义

Node.js 是⼀个基于Chrome V8引擎的JavaScript运⾏环境。Node.js使⽤了⼀个事件驱动、⾮阻塞式 I/O的模型,使其轻量又高效。

浏览器和Nodejs架构区别

图片.png

2、V8引擎

  • ⽀持语⾔:V8是⽤C ++编写的Google开源⾼性能JavaScript和WebAssembly引擎,它⽤于Chrome和Node.js等;(译:V8可以运⾏JavaScript和WebAssembly引擎编译的汇编语⾔等)
  • 跨平台:它实现ECMAScript和WebAssembly,并在Windows 7或更⾼版本,macOS 10.12+和使⽤x64,IA-32,ARM或MIPS处理器的Linux系统上运⾏;
  • 嵌⼊式:V8可以独⽴运⾏,也可以嵌⼊到任何C ++应⽤程序中;

图片.png

3、特点

  • 它是一个JavaScript运行环境;
  • 依赖于Chrome V8引擎进行代码解释执行;
  • 非阻塞I/O;
  • 轻量、可伸缩,适于实时数据交互应用。

二、CommonJS模块

CommonJS 的提出,弥补 Javascript 对于模块化,没有统⼀标准的缺陷。nodejs 借鉴了 Commonjs的 Module ,实现了良好的模块化管理。

目前CommonJS广泛应用场景:

  • Node是CommonJS在服务器端一个具有代表性的实现;
  • Browserify 是 CommonJS 在浏览器中的⼀种实现;
  • webpack 打包⼯具对 CommonJS 的⽀持和转换;也就是前端应⽤也可以在编译之前,尽情使⽤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语句处执行这个要输出的代码,而不是直接加载整个模块。
// 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的引用

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

3、源码解析

  • 源码路径:/lib/internal/modules/cjs/
//loader.js
//require函数定义
1Module.prototype.require:调用__load函数
2Module.__load:_cache处理,调用load函数
3Module.prototype.load函数:调用Module._extensions[extension](this, filename);
//不同的后缀通过定义不同的函数指定解析规则:以Module._extensions['.js']为例
4Module._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);
}
5Module.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));
      },
    });
}
  • 利用源码扩展工具
    • 后缀名扩展解析
    • 切面编程
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)
}

三、事件循环机制

JavaScript 语⾔的⼀⼤特点就是单线程,也就是说,同⼀个时间只能做⼀件事。那么,为什JavaScript 不能有多个线程呢 ?这样能提⾼效率啊。

JavaScript 的单线程,与它的⽤途有关。作为浏览器脚本语⾔,JavaScript 的主要⽤途是与⽤户互动,以及操作 DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。⽐如,假定JavaScript 同时有两个线程,⼀个线程在某个 DOM 节点上添加内容,另⼀个线程删除了这个节点,这时浏览器应该以哪个线程为准?

所以,为了避免复杂性,从⼀诞⽣,JavaScript 就是单线程,这已经成了这⻔语⾔的核⼼特征,将来也不会改变。

1、进程与线程

进程是 CPU 资源分配的最⼩单位;线程是 CPU 调度的最⼩单位。

  • 多进程:在同⼀个时间⾥,同⼀个计算机系统中如果允许两个或两个以上的进程处于运⾏状态。多进程带来的好处是明显的,⽐如你可以听歌的同时,打开编辑器敲代码,编辑器和听歌软件的进程之间丝毫不会相互⼲扰。
  • 多线程:程序中包含多个执⾏流,即在⼀个程序中可以同时运⾏多个不同的线程来执⾏不同的任务,也就是说允许单个程序创建多个并⾏执⾏的线程来完成各⾃的任务。

图片.png

2、事件循环过程

图片.png

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

每个阶段都有一个先入先出的(FIFO)的用于执行回调的队列,事件循环运行到每个阶段,都会从对应的回调队列中取出回调函数去执行,直到队列当中的内容耗尽,或者执行的回调数量达到了最大。然后事件循环就会进入下一个阶段,然后又从下一个阶段对应的队列中取出回调函数执行,这样反复直到事件循环的最后一个阶段。而事件循环也会一个一个按照循环执行,直到进程结束。

1. timer

timers阶段会执行setTimeoutsetInterval回调,并且是由poll阶段控制的。 同样,在Node中定时器指定的时间也不是准确时间,只能是尽快执行。

2. poll

poll是一个至关重要的阶段,这一阶段中,系统会做两件事情

  • 回到timer阶段执行回调

  • 执行I/O回调 并且在进入该阶段时如果没有设定了timer的话,会发生以下两件事情

  • 如果poll队列不为空,会遍历回调队列并同步执行,直到队列为空或者达到系统限制

  • 如果poll队列为空时,会有两件事发生

    • 如果有setImmediate回调需要执行,poll阶段会停止并且进入到check阶段执行回调
    • 如果没有setImmediate回调需要执行,会等待回调被加入到队列中并立即执行回调,这里同样会有个超时时间设置防止一直等待下去

当然设定了timer的话且poll队列为空,则会判断是否有timer超时,如果有的话会回到 timer 阶段执行回调。

3. check

setImmediate的回调会被加入check队列中,从event loop的阶段图可以知道,check阶段的执行顺序在poll阶段之后。

习题解析:

  • 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