Node模块原理

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:开发环境包的依赖,一个关联数组,由包的名称和版本号组成
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,378评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,356评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,702评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,259评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,263评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,036评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,349评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,979评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,469评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,938评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,059评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,703评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,257评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,262评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,501评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,792评论 2 345

推荐阅读更多精彩内容