Node模块原理分析
-
1.Node模块
- 1.1在CommonJS规范中一个文件就是一个模块
- 1.2在CommonJS规范中通过exports暴露数据
- 1.3在CommonJS规范中通过require()导入模块
-
2.Node模块原理分析
- 既然一个文件就是一个模块,既然想要使用模块必须先通过require()导入模块.所以可以推断出require()的作用其实就是读取文件,所以要想了解Node是如何实现模块的,必须先了解如何执行读取到的代码
-
3.执行从文件中读取代码
- 我们都知道通过fs模块可以读取文件,但是读取到的数据要么是二进制,要么是字符串,无论是二进制还是字符串都无法直接执行
- 但是我们知道如果是字符串,在JS中是有办法让它执行的
- eval
- 缺点:存在依赖关系,字符串可以访问外界数据,不安全
- new Function;
- 缺点:存在依赖关系,依然可以访问全局数据,不安全
- 通过NodeJS的vm虚拟机执行代码(==推荐==,具体看4)
- eval
let name1 = 'ws';
let str1 = "console.log(name1);";
eval(str1); // ws 能访问外部变量name1
let name2 = 'ws666';
let str2 = "console.log(name2);";
let fn = new Function(str2);
fn(); // ws666 能访问外部变量name2
- 4.通过NodeJS的vm虚拟机执行代码
-
runInThisContext()
- 提供了一个安全的环境给我们执行字符串中的代码
- 提供的环境==不能访问本地的变量,但是可以访问全局的变量==(也就是global上的变量)
-
runInNewContext()
无权访问外部变量,也不能访问global- 提供了一个安全的环境给我们自行字符串中的代码
- 提供的环境==不能访问本地的变量,也不能访问全局的变量==(也就是global上的变量)
-
let vm = require('vm');
// let str = 'ws';
// let res = 'console.log(str)';
// vm.runInThisContext(res); // str is not defined
// global.str = 'ws';
// let res = 'console.log(str)';
// vm.runInThisContext(res); // ws
// let str = 'ws';
// let res = 'console.log(str)';
// vm.runInNewContext(res); // str is not defined
global.str = 'ws';
let res = 'console.log(str)';
vm.runInNewContext(res); // str is not defined
Node模块官方加载流程分析
以下为扒的官方模块
- 1.内部实现了一个require方法
function require(path) {
return self.require(path);
}
- 2.通过Module对象的静态__load方法加载模块文件
Module.prototype.require = function(path) {
return Module._load(path, this, /* isMain */ false);
};
- 3.通过Module对象的静态_resolveFilename方法, 得到绝对路径并添加后缀名
var filename = Module._resolveFilename(request, parent, isMain);
- 4.根据路径判断是否有缓存,如果没有就创建一个新的Module模块对象并缓存起来
var cachedModule = Module._cache[filename];
if (cachedModule) {
return cachedModule.exports;
}
var module = new Module(filename, parent);
Module._cache[filename] = module;
function Module(id, parent) {
this.id = id;
this.exports = {};
}
-
5.利用tryModuleLoad方法加载模块
tryModuleLoad(module, filename);
5.1取出模块后缀
var extension = path.extname(filename);
5.2根据不同后缀查找不同方法并执行对应的方法, 加载模块
Module._extensions[extension](this, filename);
5.3如果是JSON就转换成对象
module.exports = JSON.parse(internalModule.stripBOM(content));
5.4如果是JS就包裹一个函数
var wrapper = Module.wrap(content); NativeModule.wrap = function(script) { return NativeModule.wrapper[0] + script + NativeModule.wrapper[1]; }; NativeModule.wrapper = [ '(function (exports, require, module, __filename, __dirname) { ', '\n});' ];
5.5执行包裹函数之后的代码,拿到执行结果(String -- Function)
var compiledWrapper = vm.runInThisContext(wrapper);
5.6利用call执行fn函数,修改module.exports的值
var args = [this.exports, require, module, filename, dirname]; var result = compiledWrapper.call(this.exports, args);
- 5.7返回module.exports
return module.exports;
EventLOOP 事件环
浏览器事件环
-
1.JS是单线程的
- JS中的代码都是串行的,前面没有执行完毕后面不能执行
-
2.执行顺序
- 2.1程序运行会从上至下依次执行所有的同步代码
- 2.2在执行的过程中如果遇到异步代码会将异步代码放到事件循环中
- 2.3当所有同步代码都执行完毕后,JS会不断检测事件循环中的异步代码是否满足条件
- 2.4一旦满足条件就执行满足条件的异步代码
-
3.宏任务和微任务
- 在JS的异步代码中又区分"宏任务(MacroTask)"和"微任务(MicroTask)"
- 宏任务:宏/大的意思,可以理解为比较费时比较慢的任务
- 微任务:微/小的意思,可以理解为相对没那么费时没那么慢的任务
- 在JS的异步代码中又区分"宏任务(MacroTask)"和"微任务(MicroTask)"
-
4.常见的宏任务和微任务
-
MacroTask
:setTimeout, setInterval, setImmediate(IE独有)...
-
MicroTask
:Promise, MutationObserver(监听节点变化) ,process.nextTick(node独有) ...
- 注意点:
- 所有的宏任务和微任务都会放到自己的执行队列中,也就是有一个宏任务队列和一个微任务队列
- 所有放到队列中的任务都采用"先进先出原则", 也就是多个任务同时满足条件,那么会先执行先放进去的
-
-
5.完整执行顺序
- 1.从上至下执行所有同步代码
- 2.==在执行过程中遇到宏任务就放到宏任务队列中,遇到微任务就放到微任务队列中==
- 3.当所有同步代码执行完毕之后,就执行微任务队列中满足需求所有回调
- 4.当微任务队列所有满足需求回调执行完毕之后, 就执行宏任务队列中满足需求所有回调
- 注意点:
- ==每执行完一个宏任务都会立刻检查微任务队列有没有被清空, 如果没有就立刻清空==
NodeJS事件环
- 1.概述
- 和浏览器中一样NodeJS中也有事件环(Event Loop),但是由于执行代码的宿主环境和应用场景不同,所以两者的事件环也有所不同.
扩展阅读: 在NodeJS中使用libuv实现了Event Loop.
源码地址: https://github.com/libuv/libuv
别看了C/C++语言写的, 你现在看不懂
- 2.NodeJS事件环和浏览器事件环区别
- 2.1任务队列个数不同
- 浏览器事件环有2个事件队列(宏任务队列和微任务队列)
- NodeJS事件环有6个事件队列
- 2.2微任务队列不同
- 浏览器事件环中有专门存储微任务的队列
- NodeJS事件环中没有专门存储微任务的队列
- 2.3微任务执行时机不同
- 浏览器事件环中每执行完一个宏任务都会去清空微任务队列
- NodeJS事件环中只有同步代码执行完毕和其它队列之间切换的时候回去清空微任务队列
- 切换队列:当队列为空(已经执行完毕或者没有满足条件回到),或者执行的回调函数数量到达系统设定的阈值时任务队列就会切换
- 2.4微任务优先级不同
- 浏览器事件环中如果多个微任务同时满足执行条件, 采用先进先出
- NodeJS事件环中如果多个微任务同时满足执行条件, 会按照优先级执行
- 在NodeJS中process.nextTick微任务的优先级高于Promise.resolve微任务
- 2.1任务队列个数不同
2.1NodeJS中的任务队列
┌───────────────────────┐
┌> │timers │执行setTimeout() 和 setInterval()中到期的callback
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │pending callbacks │执行系统操作的回调, 如:tcp, udp通信的错误callback
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │idle, prepare │ 只在内部使用
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │poll │ 执行与I/O相关的回调(除了close回调、定时器回调和setImmediate()之外,几乎所有回调都执行);
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │check │执行setImmediate的callback
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└─┤close callbacks │执行close事件的callback,例如socket.on("close",func)
└───────────────────────┘
2.2 nodejs环境中没有宏任务队列和微任务队列的概念
宏任务被放到了不同的队列中, 但是没有队列是存放微任务的队列
┌───────────────────────┐
┌> │timers │执行setTimeout() 和 setInterval()中到期的callback
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │poll │执行与I/O相关的回调(除了close回调、定时器回调和setImmediate()之外,几乎所有回调都执行);
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└─┤check │执行setImmediate的callback
└───────────────────────┘
2.3 同步代码执行完毕和其它队列之间切换的时候回去清空微任务队列
┌───────────────────────┐
| 同步代码 |
└──────────┬────────────┘
│
│ <---- 满足条件微任务代码
│
┌──────────┴────────────┐
┌> │timers │执行setTimeout() 和 setInterval()中到期的callback
│ └──────────┬────────────┘
│ │
│ │ <---- 满足条件微任务代码
│ │
│ ┌──────────┴────────────┐
│ │poll │执行与I/O相关的回调(除了close回调、定时器回调和setImmediate()之外,几乎所有回调都执行);
│ └──────────┬────────────┘
│ │
│ │ <---- 满足条件微任务代码
│ │
│ ┌──────────┴────────────┐
└─ ┤check │执行setImmediate的callback
└───────────────────────┘
- ==注意点==:
- 执行完poll, 会查看check队列是否有内容, 有就切换到check
- 如果check队列没有内容, 就会查看timers是否有内容,有就切换到timers
- 如果check队列和timers队列都没有内容,为了避免资源浪费就会==阻塞在poll==
- 执行完poll, 会查看check队列是否有内容, 有就切换到check
自定义本地包和全局包
-
1.包的规范(了解)
- package.json必须在包的顶层目录下
- 二进制文件应该在bin目录下
- JavaScript代码应该在lib目录下
- 文档应该在doc目录下
- 单元测试应该在test目录下
-
2.package.json字段分析(了解)
- name:包的名称,必须是唯一的,由小写英文字母、数字和下划线组成,不能包含空格
- description:包的简要说明
- version:符合语义化版本识别规范的版本字符串
- 主版本号:当你做了不兼容的 API 修改
- 子版本号:当你做了向下兼容的功能性新增
- 修订号:当你做了向下兼容的问题修正
- main:入口文件,一般是index.js
- scripts:指定了运行脚本命令的npm命令行缩写,默认是空的test
- keywords:关键字数组,通常用于搜索
- maintainers:维护者数组,每个元素要包含name、email(可选)、web(可选)字段
- contributors:贡献者数组,格式与maintainers相同。包的作者应该是贡献者数组的第一- 个元素
- bugs:提交bug的地址,可以是网站或者电子邮件地址
- licenses:许可证数组,每个元素要包含type(许可证名称)和url(链接到许可证文本的- 地址)字段
- repositories:仓库托管地址数组,每个元素要包含type(仓库类型,如git)、url(仓- 库的地址)和path(相对于仓库的路径,可选)字段
- dependencies:生产环境包的依赖,一个关联数组,由包的名称和版本号组成
- devDependencies:开发环境包的依赖,一个关联数组,由包的名称和版本号组成
-
3.自定义包实现步骤
- 1.创建一个包文件夹
- 2.初始化一个package.json文件
- 3.初始化一个包入口js文件
- ==注意点==:
- 如果没有配置main,默认会将index.js作为入口
- 如果包中没有index.js,那么就必须配置main
- ==注意点==:
- 4.根据包信息配置package.json文件
- 注意点:
- 通过scripts可以帮我们记住指令,然后通过npm run xxx方式就可以执行该指令
- 如果指令的名称叫做start或者test,那么执行的时候可以不加run
- 注意点:
- 5.给package.json添加bin属性,告诉系统执行全局命令时需要执行哪一个JS文件
- 6.在全局命令执行的JS文件中添加
#! /usr/bin/env node
- 7.通过
npm link
将本地包放到全局方便我们调试
-
4.将自定义包发布到官网
- 1.在https://www.npmjs.com/注册账号
- 2.在终端输入npm addUser
- 3.在终端输入npm publish