1.Node模块
(1)在CommonJs规范中一个文件就是一个模块
(2)在CommonJS规范中通过exports暴露数据
(3)在CommonJS规范中通过require()导入模块
2.Node模块分析
NodeJs既然一个文件就是一个模块,想要使用模块必须先通过require()导入模块
所以可以推断出require()的作用其实就是读取文件
3.执行文件中读取代码
(1)我们都知道通过fs模块可以读取文件,但是读取到的数据要么是二进制, 要么是字符串
无论是二进制还是字符串都无法直接执行。但是我们知道如果将其转为字符串, 在JS中通过eval() 或者 new Function()执行。
let str = "console.log('卢春琼')";
eval(str);//卢春琼
let name = "luchunqiong";
let str = "console.log(name)";
eval(str);//luchunqiong 通过name变量找到了值,存在依赖关系
******************************************************************************************
let str = "console.log('www.luchunqiong.com');";
let fn = new Function(str);
fn();//www.luchunqiong.com
let name = "luchunqiong";
let str = "console.log(name);";
let fn = new Function(str);
fn();//luchunqiong 通过name变量找到了值,存在依赖关系
*****************************************************************************************
(2)通过eval()和 new Function()执行代码都有这样的缺点: 存在依赖关系, 字符串可以访问外界数据,不安全,所以我们需要通过NodeJS的VM虚拟机执行代码
let vm = require("vm");
//runInThisContext: 提供了一个安全的环境给我们自行字符串中的代码
//runInThisContext提供的环境不能访问本地的变量, 但是可以访问全局的变量(也就是global上的变量)
let str = "console.log('www.luchunqiong.com');";
vm.runInThisContext(str); //www.luchunqiong.com
let name = "lnj";
let str = "console.log(name);";
vm.runInThisContext(str); // name is not defined
global.name = "luchunqiong";
let str = "console.log(name);";
vm.runInThisContext(str);//luchunqiong
*****************************************************************************************
//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
总结:所以为了数据安全,执行代码时我们尽量使用 runInNewContext()方法
4.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方法, 得到绝对路径并添加后缀名
let filename = Module._resolveFilename(request, parent, isMain);
4.根据路径判断是否有缓存, 如果没有就创建一个新的Module模块对象并缓存起来
let cachedModule = Module._cache[filename];
if (cachedModule) {
return cachedModule.exports;
}
let 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取出模块后缀
let extension = path.extname(filename);
5.2根据不同后缀查找不同方法并执行对应的方法, 加载模块
Module._extensions[extension](this, filename);
5.3如果是JSON就转换成对象
module.exports = JSON.parse(internalModule.stripBOM(content));
5.4如果是JS就包裹一个函数
let 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)
let compiledWrapper = vm.runInThisContext(wrapper);
5.6利用call执行fn函数, 修改module.exports的值
let args = [this.exports, require, module, filename, dirname];
let result = compiledWrapper.call(this.exports, args);
5.7返回module.exports
return module.exports;
5.手写Node简易模块
let path = require("path");
let fs = require("fs");
let vm = require("vm");
class MYModule {
constructor(id){
this.id = id; // 保存当前模块的绝对路径
this.exports = {};
}
}
MYModule._cache = {};
MYModule._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 = MYModule.wrapper[0] + script + MYModule.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;
}
};
MYModule.wrapper = [
'(function (exports, require, module, __filename, __dirname) { ',
'\n});'
];
function njRequire(filePath) {
// 1.将传入的相对路径转换成绝对路径
let absPath = path.join(__dirname, filePath);
// 2.尝试从缓存中获取当前的模块
let cachedModule = MYModule._cache[absPath];
if (cachedModule) {
return cachedModule.exports;
}
// 3.如果没有缓存就自己创建一个MYModule对象, 并缓存起来
let module = new MYModule(absPath);
MYModule._cache[absPath] = module;
// 4.利用tryModuleLoad方法加载模块
tryModuleLoad(module);
// 5.返回模块的exports
return module.exports
}
function tryModuleLoad(module){
// 1.取出模块后缀
let extName = path.extname(module.id);
MYModule._extensions[extName](module);
}
// let aModule = njRequire("./person.json");
let aModule = njRequire(".index.js");
console.log(aModule);
6.浏览器事件环(事件池)
(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.当微任务队列所有满足需求回调执行完毕之后, 就执行宏任务队列中满足需求所有回调
注意点:
每执行完一个宏任务都会立刻检查微任务队列有没有被清空, 如果没有就立刻清空
console.log("start");
setTimeout(function () {
console.log("setTimeout1");
Promise.resolve().then(function () {
console.log("Promise2");
});
Promise.resolve().then(function () {
console.log("Promise3");
});
}, 0);
console.log("middle");
Promise.resolve().then(function () {
console.log("Promise1");
setTimeout(function () {
console.log("setTimeout2");
});
setTimeout(function () {
console.log("setTimeout3");
});
});
console.log("end");
//start middle end Promise setTimeout1 Promise2 Promise3 setTimeout2 setTimeout3
7.Node事件环
(1)和浏览器中一样NodeJS中也有事件环(Event Loop),但是由于执行代码的宿主环境和应用场景不同,所以两者的事件环也有所不同
扩展阅读: 在NodeJS中使用libuv实现了Event Loop.
源码地址: https://github.com/libuv/libuv
个人觉得别看C/C++语言写的, 看不懂
(2)NodeJS事件环和浏览器事件环区别
A:任务队列个数不同
浏览器事件环有2个事件队列(宏任务队列和微任务队列)NodeJS事件环有6个事件队列
B:微任务队列不同
浏览器事件环中有专门存储微任务的队列,NodeJS事件环中没有专门存储微任务的队列
C:微任务执行时机不同
浏览器事件环中每执行完一个宏任务都会去清空微任务队列,NodeJS事件环中只有同步代码执行完毕和其它队列之间切换的时候回去清空微任务队列
D:微任务优先级不同
浏览器事件环中如果多个微任务同时满足执行条件, 采用先进先出NodeJS事件环中如果多个微任务同时满足执行条件, 会按照优先级执行
(3)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)
└───────────────────────┘
(4)Node事件环的执行特点
A:和浏览器不同的是没有宏任务队列和微任务队列的概念,宏任务被放到了不同的队列中, 但是没有队列是存放微任务的队列,微任务会在执行完同步代码和队列切换的时候执行
B:什么时候切换队列?
当队列为空(已经执行完毕或者没有满足条件回到)或者执行的回调函数数量到达系统设定的阈值时任务队列就会切换
C:在NodeJS中process.nextTick微任务的优先级高于Promise.resolve微任务
D:执行完poll, 会查看check队列是否有内容, 有就切换到check,如果check队列没有内容, 就会查看timers是否有内容, 有就切换到timers,如果check队列和timers队列都没有内容, 为了避免资源浪费就会阻塞在poll
//Node事件环练习
setTimeout(function () {
console.log("setTimeout1");
Promise.resolve().then(function () {
console.log("Promise1");
});
process.nextTick(function () {
console.log("process.nextTick1");
});
});
console.log("Start");
setTimeout(function () {
console.log("setTimeout2");
Promise.resolve().then(function () {
console.log("Promise2");
});
process.nextTick(function () {
console.log("process.nextTick2");
});
});
console.log("End");
8.Node面试题
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文件在执行的时候都会被包裹到一个函数中, 这些属性都被通过参数的形式传递过来了
let 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.注意点: 在NodeJS中指定的延迟时间是有一定的误差的, 所以导致了输出结果随机的问题
//如下代码输出的结果是随机的
setTimeout(function () {
console.log("setTimeout");
}, 0);
setImmediate(function () {
console.log("setImmediate");
});
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
//以下代码执行后 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");
});
});
9.Node包的特点
(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:开发环境包的依赖,一个关联数组,由包的名称和版本号组成