Node中的模块机制

image.png

1.2 请介绍一下require的模块加载机制

这道题基本上就可以了解到面试者对Node模块机制的了解程度 基本上面试提到

  • 1、先计算模块路径
  • 2、如果模块在缓存里面,取出缓存
  • 3、加载模块
  • 4、输出模块的exports属性即可 ```javascript // require 其实内部调用 Module._load 方法 Module._load = function(request, parent, isMain) { // 计算绝对路径 var filename = Module._resolveFilename(request, parent);

    // 第一步:如果有缓存,取出缓存 var cachedModule = Module._cache[filename]; if (cachedModule) { return cachedModule.exports;

    // 第二步:是否为内置模块 if (NativeModule.exists(filename)) { return NativeModule.require(filename); }

    /**这里注意了**/ // 第三步:生成模块实例,存入缓存 // 这里的Module就是我们上面的1.1定义的Module var module = new Module(filename, parent); Module._cache[filename] = module;

    /**这里注意了**/ // 第四步:加载模块 // 下面的module.load实际上是Module原型上有一个方法叫Module.prototype.load try { module.load(filename); hadException = false; } finally { if (hadException) {

    1. delete Module._cache[filename];

    } }

    // 第五步:输出模块的exports属性 return module.exports; };

  1. <a name="ulUX4"></a>
  2. # module.exports与exports,export与export default之间的关系和区别
  3. 首先我们要明白一个前提,CommonJS模块规范和ES6模块规范完全是两种不同的概念。
  4. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/8414423/1641958392176-ee950ae4-b8cd-4360-910b-1da174737430.png#clientId=u308cb0c2-823c-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=911&id=ud121178d&margin=%5Bobject%20Object%5D&name=image.png&originHeight=911&originWidth=1282&originalType=binary&ratio=1&rotation=0&showTitle=false&size=145181&status=done&style=none&taskId=ub0c615ea-d2af-4138-ad4f-a3d040af6af&title=&width=1282)
  5. ![image.png](https://cdn.nlark.com/yuque/0/2022/png/8414423/1641958449046-212d3ae1-34fb-4be5-a423-e5f152f9d6d0.png#clientId=u308cb0c2-823c-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=738&id=u3f231c41&margin=%5Bobject%20Object%5D&name=image.png&originHeight=738&originWidth=1284&originalType=binary&ratio=1&rotation=0&showTitle=false&size=121795&status=done&style=none&taskId=u177f550e-1efa-4ad2-afd0-1a9997ebbe3&title=&width=1284)
  6. ```javascript
  7. module.exports vs exports
  8. 很多时候,你会看到,在Node环境中,有两种方法可以在一个模块中输出变量:
  9. 方法一:对module.exports赋值:
  10. // hello.js
  11. function hello() {
  12. console.log('Hello, world!');
  13. }
  14. function greet(name) {
  15. console.log('Hello, ' + name + '!');
  16. }
  17. module.exports = {
  18. hello: hello,
  19. greet: greet
  20. };
  21. 方法二:直接使用exports:
  22. // hello.js
  23. function hello() {
  24. console.log('Hello, world!');
  25. }
  26. function greet(name) {
  27. console.log('Hello, ' + name + '!');
  28. }
  29. function hello() {
  30. console.log('Hello, world!');
  31. }
  32. exports.hello = hello;
  33. exports.greet = greet;
  34. 但是你不可以直接对exports赋值:
  35. // 代码可以执行,但是模块并没有输出任何变量:
  36. exports = {
  37. hello: hello,
  38. greet: greet
  39. };
  40. 如果你对上面的写法感到十分困惑,不要着急,我们来分析Node的加载机制:
  41. 首先,Node会把整个待加载的hello.js文件放入一个包装函数load中执行。在执行这个load()函数前,Node准备好了module变量:
  42. var module = {
  43. id: 'hello',
  44. exports: {}
  45. };
  46. load()函数最终返回module.exports:
  47. var load = function (exports, module) {
  48. // hello.js的文件内容
  49. ...
  50. // load函数返回:
  51. return module.exports;
  52. };
  53. var exportes = load(module.exports, module);
  54. 也就是说,默认情况下,Node准备的exports变量和module.exports变量实际上是同一个变量,并且初始化为空对象{},于是,我们可以写:
  55. exports.foo = function () { return 'foo'; };
  56. exports.bar = function () { return 'bar'; };
  57. 也可以写:
  58. module.exports.foo = function () { return 'foo'; };
  59. module.exports.bar = function () { return 'bar'; };
  60. 换句话说,Node默认给你准备了一个空对象{},这样你可以直接往里面加东西。
  61. 但是,如果我们要输出的是一个函数或数组,那么,只能给module.exports赋值:
  62. module.exports = function () { return 'foo'; };
  63. 给exports赋值是无效的,因为赋值后,module.exports仍然是空对象{}。
  64. 结论
  65. 如果要输出一个键值对象{},可以利用exports这个已存在的空对象{},并继续在上面添加新的键值;
  66. 如果要输出一个函数或数组,必须直接对module.exports对象赋值。
  67. 所以我们可以得出结论:直接对module.exports赋值,可以应对任何情况:
  68. module.exports = {
  69. foo: function () { return 'foo'; }
  70. };
  71. 或者:
  72. module.exports = function () { return 'foo'; };
  73. 最终,我们强烈建议使用module.exports = xxx的方式来输出模块变量,这样,你只需要记忆一种方法。

image.png

垃圾回收

计算机的动态内存不再需要的时候就应该释放,让出内存。

  • 引用计数
  1. let user={
  2. name:"simple"
  3. }
  4. //现在user就引用了{name:"simple"}这个对象
  5. user=null;
  6. //现在{name:"simple"}变成不可到达的了,不能访问,JS就会回收他
  7. //若是
  8. let user={
  9. name:"simple"
  10. }
  11. let admin=usr;
  12. //现在{name:"simple"}就被两个对象引用
  13. user=null;
  14. //即使执行这个,{name:"simple"}还可以通过admin获取,故未被回收
  15. //循环引用问题
  16. function func() {
  17. let obj1 = {};
  18. let obj2 = {};
  19. obj1.a = obj2; // obj1 引用 obj2
  20. obj2.a = obj1; // obj2 引用 obj1
  21. }
  22. // 当函数 func 执行结束后,返回值为 undefined,所以整个函数以及内部的变量都应该被回收,但根据引用计数方法,obj1 和 obj2 的引用次数都不为 0,所以他们不会被回收。
  23. // 要解决循环引用的问题,最好是在不使用它们的时候手工将它们设为空。上面的例子可以这么做:
  24. obj1 = null;
  25. obj2 = null;
  • 标记清除

解决引用计数解决不了的问题的,就是无法清除循环引用,标记清除分为标记与清除两个阶段,首先遍历所有数据,将能可达或者间接可达的数据标记存活,然后再清理没有标记的数据,这样就完成一次清理,一般都是引用计数为主,标记清除为辅,来清除垃圾

  • 新老生代 新生代(scavenge算法) 老生代(标记清除)
  1. 各种实例对象存放在堆中,然后堆又划分成了两个不同的区域 年轻代(新生代区、from区、to区)和年老代
  2. 新建的对象总是在新生代存放在新生代区中,当新生代区已经满了,就会触发一次垃圾回收(Garbage Collection)。
  3. 将新生代中还未被使用的对象(存活的对象)复制到From区,当新生代区空间再次满,会触发一次GC,就会将新生代区和from区的未使用对象复制到To
  4. 下一次垃圾回收会将to区和新生代区的未使用对象复制到from区,这样某些对象就会在fromto区多次复制,如果超过某个阈值对象
  5. 还没有被释放的话,就将改对象复制到老年代区
  6. 当一个对象经过多次复制依然存活的时候,他会被认为是生命周期比较长的对象,会被移动到老生代中. 从新生代移动到老生代的过程称为晋升

scavenge只复制存活的对象,而标记清除只清除死亡的对象,活对象在新生代中占比较小,死对象在老生代中占比较小。

  • node内存限制
    由于V8内存限制,无法通过fs.readFile和fs.writeFile进行大文件的操作,从而改用fs.createReadStream和fs.createWriteStream通过流的方式实现对大文件的操作

    1.加载和执行

浏览器在执行JavaScript代码的时候,不能做其他任何事情.JavaScript 执行过程耗时越久,浏览器等待响应用户输入的时间就越长。

这意味着 script 标签每次出现都霸道地让页面等待脚本的解析和执行。无论当前的JavaScript代码是内嵌的还是包含在外链文件中,页面的下载和渲染都必须停下来等待脚本执行完成

  • 性能问题
  1. script标签放在head中,或者放到body的前面,会导致脚本阻塞页面的渲染
  • 浏览器厂商的解决
  1. 允许并行下载JavaScript,但是JavaScript的下载过程任然会阻塞其他资源的下载,比如图片
  2. 尽管脚本下载过程不会互相影响,但是页面仍然要等待所有JavaScript代码下载完成之后才能继续
  • 解决方案
  1. 1. script标签放在body的后面
  2. 2. 将多个脚本合并成一个脚本.考虑到HTTP请求会带来额外的性能开销,因此下载单个100KB的文件将比下载425KB的文件更快。也就是说,减少页面中外链脚本文件的数量将会改善性能
  3. 3. 减小JavaScript文件大小 压缩 uglyify 不必要的空格注释等等
  4. 4. defer属性,在解析到JavaScript标签的时候开始下载,但是不会执行,直到DOM加载完成才执行