参考资料:
1、node事件循环
2、V8 引擎源码
3、node源码
一、背景
1、定义
Node.js 是⼀个基于Chrome V8引擎的JavaScript运⾏环境。Node.js使⽤了⼀个事件驱动、⾮阻塞式 I/O的模型,使其轻量又高效。
浏览器和Nodejs架构区别
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 ++应⽤程序中;
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函数定义
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;
}
6、function 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 调度的最⼩单位。
- 多进程:在同⼀个时间⾥,同⼀个计算机系统中如果允许两个或两个以上的进程处于运⾏状态。多进程带来的好处是明显的,⽐如你可以听歌的同时,打开编辑器敲代码,编辑器和听歌软件的进程之间丝毫不会相互⼲扰。
- 多线程:程序中包含多个执⾏流,即在⼀个程序中可以同时运⾏多个不同的线程来执⾏不同的任务,也就是说允许单个程序创建多个并⾏执⾏的线程来完成各⾃的任务。
2、事件循环过程
- 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
阶段会执行setTimeout
和setInterval
回调,并且是由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