Node模块原理
1.node模块原理分析
<body>
<!--
1.Node模块
1.1在CommonJS规范中一个文件就是一个模块
1.2在CommonJS规范中通过exports暴露数据
1.3在CommonJS规范中通过require()导入模块
2.Node模块原理分析
既然一个文件就是一个模块,
既然想要使用模块必须先通过require()导入模块
所以可以推断出require()的作用其实就是读取文件
所以要想了解Node是如何实现模块的, 必须先了解如何执行读取到的代码
3.执行从文件中读取代码
我们都知道通过fs模块可以读取文件,
但是读取到的数据要么是二进制, 要么是字符串
无论是二进制还是字符串都无法直接执行
但是我们知道如果是字符串, 在JS中是有办法让它执行的
eval 或者 new Function;
4.通过eval执行代码
缺点: 存在依赖关系, 字符串可以访问外界数据,不安全
5.通过new Function执行代码
缺点: 存在依赖关系, 依然可以访问全局数据,不安全
-->
<!--
6.通过NodeJS的vm虚拟机执行代码
runInThisContext: 无权访问外部变量, 但是可以访问global
runInNewContext: 无权访问外部变量, 也不能访问global
-->
<script>
// let str = "console.log('www.it666.com');";
// eval(str);
// 存在依赖关系, 字符串可以访问外界数据,不安全
// let name = "lnj";
// let str = "console.log(name);";
// eval(str);
// let str = "console.log('www.it666.com');";
// let fn = new Function(str);
// console.log(fn);
// fn();
// 存在依赖关系, 字符串可以访问外界数据,不安全
let name = "lnj";
let str = "console.log(name);";
let fn = new Function(str);
fn();
</script>
</body>
js代码
let vm = require("vm");
// let str = "console.log('www.it666.com');";
// vm.runInThisContext(str);
/*
runInThisContext: 提供了一个安全的环境给我们自行字符串中的代码
runInThisContext提供的环境不能访问本地的变量, 但是可以访问全局的变量(也就是global上的变量)
* */
// let name = "lnj";
// let str = "console.log(name);";
// vm.runInThisContext(str); // name is not defined
// global.name = "lnj";
// let str = "console.log(name);";
// vm.runInThisContext(str);
/*
runInNewContext: 提供了一个安全的环境给我们执行字符串中的代码
runInNewContext提供的环境不能访问本地的变量, 也不能访问全局的变量(也就是global上的变量)
* */
// let name = "lnj";
// let str = "console.log(name);";
// vm.runInNewContext(str); // name is not defined
global.name = "lnj";
let str = "console.log(name);";
vm.runInNewContext(str); // name is not defined
2.node模块加载分析(多看几遍视频)
<body>
<!--
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);
- 6.1取出模块后缀
var extension = path.extname(filename);
- 6.2根据不同后缀查找不同方法并执行对应的方法, 加载模块
Module._extensions[extension](this, filename);
- 6.3如果是JSON就转换成对象
module.exports = JSON.parse(internalModule.stripBOM(content));
- 6.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});'
];
- 6.5执行包裹函数之后的代码, 拿到执行结果(String -- Function)
var compiledWrapper = vm.runInThisContext(wrapper);
- 6.6利用call执行fn函数, 修改module.exports的值
var args = [this.exports, require, module, filename, dirname];
var result = compiledWrapper.call(this.exports, args);
- 6.7返回module.exports
return module.exports;
-->
</body>
3.自己实现一下(多看视频多分析)
let path = require("path");
let fs = require("fs");
let vm = require("vm");
class NJModule {
constructor(id){
this.id = id; // 保存当前模块的绝对路径
this.exports = {};
}
}
NJModule._cache = {};
NJModule._extensions = {
".js": function (module) {
// 1.读取JS代码
let script = fs.readFileSync(module.id);
// 2.将JS代码包裹到函数中
/*
(function (exports, require, module, __filename, __dirname) {
exports.name = "lnj";
});
* */
let strScript = NJModule.wrapper[0] + script + NJModule.wrapper[1];
// 3.将字符串转换成JS代码
let jsScript = vm.runInThisContext(strScript);
// 4.执行转换之后的JS代码
// var args = [this.exports, require, module, filename, dirname];
// var result = compiledWrapper.call(this.exports, args);
jsScript.call(module.exports, module.exports);
},
".json": function (module) {
let json = fs.readFileSync(module.id);
let obj = JSON.parse(json);
module.exports = obj;
}
};
NJModule.wrapper = [
'(function (exports, require, module, __filename, __dirname) { ',
'\n});'
];
function njRequire(filePath) {
// 1.将传入的相对路径转换成绝对路径
let absPath = path.join(__dirname, filePath);
// 2.尝试从缓存中获取当前的模块
let cachedModule = NJModule._cache[absPath];
if (cachedModule) {
return cachedModule.exports;
}
// 3.如果没有缓存就自己创建一个NJModule对象, 并缓存起来
let module = new NJModule(absPath);
NJModule._cache[absPath] = module;
// 4.利用tryModuleLoad方法加载模块
tryModuleLoad(module);
// 5.返回模块的exports
return module.exports
}
function tryModuleLoad(module){
// 1.取出模块后缀
let extName = path.extname(module.id);
NJModule._extensions[extName](module);
}
// let aModule = njRequire("./person.json");
let aModule = njRequire("./02-a.js");
console.log(aModule);
4.根据上面的流程,来回答几个面试题
<body>
<!--
1.NodeJS中的this为什么是一个空对象?
-->
<!--
因为所有的NodeJS文件在执行的时候都会被包裹到一个函数中, this都被修改为了空的module.exports
(function (exports, require, module, __filename, __dirname) {
// 我们编写的代码
// 所以说在这里面拿到的this就是 空的module.exports
});
compiledWrapper.call(module.exports, args);
-->
<!--
2.NodeJS中为什么可以直接使用exports, require, module, __filename, __dirname
-->
<!--
因为所有的NodeJS文件在执行的时候都会被包裹到一个函数中, 这些属性都被通过参数的形式传递过来了
var args = [module.exports, require, module, filename, dirname];
compiledWrapper.call(this.exports, args);
-->
<!--
3.NodeJS中为什么不能直接exports赋值, 而可以给module.exports赋值
-->
<!--
(function (exports, require, module, __filename, __dirname) {
exports = "lnj";
});
jsScript.call(module.exports, module.exports);
return module.exports;
相当于
let exports = module.exports;
exports = "lnj";
return module.exports;
-->
<!--
4.通过require导入包时候应该使用var/let还是const?
导入包的目的是使用包而不是修改包, 所以导入包时使用const接收
-->
</body>
5.浏览器事件环
<body>
<!--
1.JS是单线程的
JS中的代码都是串行的, 前面没有执行完毕后面不能执行
2.执行顺序
2.1程序运行会从上至下依次执行所有的同步代码
2.2在执行的过程中如果遇到异步代码会将异步代码放到事件循环中
2.3当所有同步代码都执行完毕后, JS会不断检测 事件循环中的异步代码是否满足条件
2.4一旦满足条件就执行满足条件的异步代码
3.宏任务和微任务
在JS的异步代码中又区分"宏任务(MacroTask)"和"微任务(MicroTask)"
宏任务: 宏/大的意思, 可以理解为比较费时比较慢的任务
微任务: 微/小的意思, 可以理解为相对没那么费时没那么慢的任务
4.常见的宏任务和微任务
MacroTask: setTimeout, setInterval, setImmediate(IE独有)...
MicroTask: Promise, MutationObserver ,process.nextTick(node独有) ...
注意点: 所有的宏任务和微任务都会放到自己的执行队列中, 也就是有一个宏任务队列和一个微任务队列
所有放到队列中的任务都采用"先进先出原则", 也就是多个任务同时满足条件, 那么会先执行先放进去的
5.完整执行顺序
1.从上至下执行所有同步代码
2.在执行过程中遇到宏任务就放到宏任务队列中,遇到微任务就放到微任务队列中
3.当所有同步代码执行完毕之后, 就执行微任务队列中满足需求所有回调
4.当微任务队列所有满足需求回调执行完毕之后, 就执行宏任务队列中满足需求所有回调
... ...
注意点:
每执行完一个宏任务都会立刻检查微任务队列有没有被清空, 如果没有就立刻清空
-->
<div></div>
<button class="add">添加节点</button>
<button class="del">删除节点</button>
<script>
//根据上面的解释分析以下代码的输出顺序
/*
// setImmediate和setTimeout, setInterval区别:
// setImmediate不能设置延迟时间, 并且只能执行一次
setImmediate(function () {
console.log("setImmediate");
});
console.log("同步代码Start");
console.log("同步代码End");
*/
/*
// MutationObserver是专门用于监听节点的变化
let oDiv = document.querySelector("div");
let oAddBtn = document.querySelector(".add");
let oDelBtn = document.querySelector(".del");
oAddBtn.onclick = function () {
let op = document.createElement("p");
op.innerText = "我是段落";
oDiv.appendChild(op);
}
oDelBtn.onclick = function () {
let op = document.querySelector("p");
oDiv.removeChild(op);
}
let mb = new MutationObserver(function () {
console.log("执行了");
});
mb.observe(oDiv, {
"childList": true
});
console.log("同步代码Start");
console.log("同步代码End");
*/
/*
// 1.定义一个宏任务
setTimeout(function () {
console.log("setTimeout1");
}, 0);
// 2.定义一个微任务
Promise.resolve().then(function () {
console.log("Promise1");
});
console.log("同步代码Start");
Promise.resolve().then(function () {
console.log("Promise2");
});
setTimeout(function () {
console.log("setTimeout2");
}, 0);
console.log("同步代码End");
*/
/*
// 1.定义一个宏任务
setTimeout(function () {
console.log("setTimeout1");
// 2.定义一个微任务 p1
Promise.resolve().then(function () {
console.log("Promise1");
});
// 2.定义一个微任务 p2
Promise.resolve().then(function () {
console.log("Promise2");
});
}, 0);
// 1.定义一个宏任务
setTimeout(function () {
console.log("setTimeout2");
// 2.定义一个微任务 p3
Promise.resolve().then(function () {
console.log("Promise3");
});
// 2.定义一个微任务 p4
Promise.resolve().then(function () {
console.log("Promise4");
});
}, 0);
*/
/*
// 1.定义一个宏任务
setTimeout(function () {
console.log("setTimeout1");
// 2.定义一个微任务 p2
Promise.resolve().then(function () {
console.log("Promise2");
});
// 2.定义一个微任务 p3
Promise.resolve().then(function () {
console.log("Promise3");
});
}, 0);
// 2.定义一个微任务 p3
Promise.resolve().then(function () {
console.log("Promise1");
// s2
setTimeout(function () {
console.log("setTimeout2");
});
// s3
setTimeout(function () {
console.log("setTimeout3");
});
});
*/
</script>
</body>
6.nodeJs事件环
<body>
<!--
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事件环中如果多个微任务同时满足执行条件, 会按照优先级执行
2.NodeJS中的任务队列
┌───────────────────────┐
┌> │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)
└───────────────────────┘
┌───────────────────────┐
┌> │timers │执行setTimeout() 和 setInterval()中到期的callback
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │poll │执行与I/O相关的回调
│ (除了close回调、定时器回调和setImmediate()之外,几乎所有回调都执行);
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└─┤check │执行setImmediate的callback
└───────────────────────┘
1.注意点:
和浏览器不同的是没有宏任务队列和微任务队列的概念
宏任务被放到了不同的队列中, 但是没有队列是存放微任务的队列
微任务会在执行完同步代码和队列切换的时候执行
什么时候切换队列?
当队列为空(已经执行完毕或者没有满足条件回到)
或者执行的回调函数数量到达系统设定的阈值时任务队列就会切换
2.注意点:
在NodeJS中process.nextTick微任务的优先级高于Promise.resolve微任务
-->
<!--
┌───────────────────────┐
│ 同步代码
└──────────┬────────────┘
│
│ <---- 满足条件微任务代码
│
┌──────────┴────────────┐
┌> │timers │执行setTimeout() 和 setInterval()中到期的callback
│ └──────────┬────────────┘
│ │
│ │ <---- 满足条件微任务代码
│ │
│ ┌──────────┴────────────┐
│ │poll │执行与I/O相关的回调
│ │ (除了close回调、定时器回调和setImmediate()之外,几乎所有回调都执行);
│ └──────────┬────────────┘
│ │
│ │ <---- 满足条件微任务代码
│ │
│ ┌──────────┴────────────┐
└─┤check │执行setImmediate的callback
└───────────────────────┘
注意点:
执行完poll, 会查看check队列是否有内容, 有就切换到check
如果check队列没有内容, 就会查看timers是否有内容, 有就切换到timers
如果check队列和timers队列都没有内容, 为了避免资源浪费就会阻塞在poll
-->
<script>
//同样分析以下代码的打印顺序
/*
Promise.resolve().then(function () {
console.log("Promise");
});
process.nextTick(function () {
console.log("process.nextTick1");
});
process.nextTick(function () {
console.log("process.nextTick2");
});
process.nextTick(function () {
console.log("process.nextTick3");
});
*/
/*
setTimeout(function () {
console.log("setTimeout");
});
Promise.resolve().then(function () {
console.log("Promise");
});
console.log("同步代码 Start");
process.nextTick(function () {
console.log("process.nextTick");
});
setImmediate(function () {
console.log("setImmediate");
});
console.log("同步代码 End");
*/
/*
setTimeout(function () {
console.log("setTimeout1");
// p1
Promise.resolve().then(function () {
console.log("Promise1");
});
// n1
process.nextTick(function () {
console.log("process.nextTick1");
});
});
console.log("同步代码 Start");
setTimeout(function () {
console.log("setTimeout2");
// p2
Promise.resolve().then(function () {
console.log("Promise2");
});
// n2
process.nextTick(function () {
console.log("process.nextTick2");
});
});
console.log("同步代码 End");
*/
/*
注意点: 如下代码输出的结果是随机的
在NodeJS中指定的延迟时间是有一定的误差的, 所以导致了输出结果随机的问题
*/
/*
setTimeout(function () {
console.log("setTimeout");
}, 0);
setImmediate(function () {
console.log("setImmediate");
});
*/
const path = require("path");
const fs = require("fs");
fs.readFile(path.join(__dirname, "04.js"), function () {
setTimeout(function () {
console.log("setTimeout");
}, 0);
setImmediate(function () {
console.log("setImmediate");
});
});
</script>
</body>
7.自定义本地包和全局包
<body>
<!--
1.包的规范(了解)
- package.json必须在包的顶层目录下
- 二进制文件应该在bin目录下
- JavaScript代码应该在lib目录下
- 文档应该在doc目录下
- 单元测试应该在test目录下
2.package.json字段分析(了解)
- name:包的名称,必须是唯一的,由小写英文字母、数字和下划线组成,不能包含空格
- description:包的简要说明
- version:符合语义化版本识别规范的版本字符串
+ 主版本号:当你做了不兼容的 API 修改
+ 子版本号:当你做了向下兼容的功能性新增
+ 修订号:当你做了向下兼容的问题修正
- 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
-->
</body>