最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • Node 模块机制不完全指北

    正文概述 掘金(拜小白)   2021-04-23   466

    引言


    Node 中采用了 CommonJs 规范,通过 module.exports 和 require 来导出导入模块。在模块加载机制中,Node 采用延迟加载的策略,只有到使用到时,才会去加载,加载之后会被存入 cache 中。

    面试中关于模块机制的常见问题


    • require 的加载机制?
    • 假设有 a.js、b.js 两个模块相互引用,会有什么问题?是否为陷入死循环?
    • a 模块中的 undeclaredVariable 变量在 b.js 中是否会被打印?
    • module.exports 与 exports 的区别
    • import 和 require 的区别
    • 模块在 require 的过程中是同步还是异步?

    Node 模块分类


    1. 核心模块

    在 Node 中有一些用二进制发布的模块,称之为核心模块。核心模块只能通过模块名来引用,而不能通过文件路径来引用,即使已经存在一个与其同名的第三方模块,也会优先加载核心模块。

    2. 文件模块

    可以通过绝对路径从文件系统中加载非核心模块。

    const module = require('../my_module/module');
    

    通过路径加载模块时可以省略.js后缀,如果没有找到这个文件,Node 会在文件名后面加上.js再次查找路径。

    3. 文件夹模块

    const module = require('../my_module');
    

    Node 会在指定的文件夹中查找模块。Node 会假设当前文件夹是一个包含了 package.json 的包定义。

    • 如果 package.json 存在就会解析文件,查找 main 属性,并将 main 属性作为入口点。
    • 如果 package.json 不存在,就会将包入口点假设为 index.js
    4. 加载 node_modules 模块

    如果在核心模块和文件模块都没有找到模块,Node 就会尝试在当前目录下的 node_modules 文件夹中查找该模块。

    const myModule = require('myModule.js');
    
    1. 尝试找到文件 './node_modules/myModules.js'
    2. 如果没有找到,就会查找上一级目录 '../node_modules/myModules.js'
    3. 如果还是没有找到,就在往上一级目录,直到找到根目录或者找到为止
    5. 缓存模块

    模块在首次加载时,会被缓存起来,意味着如果模块名能解析为相同的文件名,那么在此调用时,都会返回同一模块。

    // myModules.js
    console.log('modules start initializing...');
    modules.exports = function() {
        console.log(1);
    }
    console.log('modules end initializing...');
    
    // 一次加载
    var myModules = require('./myModules.js');
    // 输出
    // modules start initializing...
    // modules end initializing...
    
    // 两次加载
    var myModules = require('./myModules.js');
    var myModules = require('./myModules.js');
    // 还是输出
    // modules start initializing...
    // modules end initializing...
    // 意味着模块的初始化只会被初始化一次
    

    require 模块加载机制


    Node 的模块加载机制,分为三个步骤:路径解析、文件定位、编译执行

    Node 模块机制不完全指北

    1. 缓存模块:模块被加载之后会放入到缓存区,模块执行首先是先进行缓存加载,判断缓存中是否有值。
    2. 原生模块:仅次于缓存模块,原生模块被编译成二进制,省略了文件定位、编译执行,直接加载到了内存中。
    3. 文件模块:优先加载 .、..、/ 开头的,如果文件没有加上扩展名,会依次按照 .js、.json、.node 进行扩展名补足尝试,那么在尝试的过程中也是以同步阻塞模式来判断文件是否存在,从性能优化的角度来看待,.json、.node最好还是加上文件的扩展名。
    4. 文件夹模块:这种情况发生在文件模块查询的过程中,发现没有找到文件模块,但是路径是一个文件夹时,这时候会将目录作为一个包处理。先在包的根目录查找 package.json 文件夹,如果没有就会找index.js,如果有就会解析文件,找 main 属性描述的文件进行加载,如果加载文件没有加载到就会报错。
    5. node_modules模块:当上诉模块都没有找到的时候就会在当前目录的父目录的 node_modules 中查找如果没有找到就在在往上次目录中查找,直到找到或者找到根目录结束。

    require 文件加载机制


    关于文件模块的加载,大家可以直接看这张图,我就不多讲了,很清晰。

    Node 模块机制不完全指北

    require 加载模块的时候是同步还是异步?


    先回答问题,同步的! 但是面试官要是问你为什么是同步还是异步的呢? 其实这个答案并不是固定的,但是小伙伴们可以通过这几方面给面试官解释。

    1. 一个作为公共依赖的模块,当然想一次加载出来,同步更好
    2. 模块的个数往往是有限的,而且 Node.js 在 require 的时候会自动缓存已经加载的模块,再加上访问的都是本地文件,产生的IO开销几乎可以忽略。所以不需要异步,同步就好。

    require加载模块会先运行目标文件


    当在文件中 require 某个模块时,会从头开始先运行目标文件。

    列子1
    // a.js
    let a = 1;
    a = a + 1;
    module.exports = a;
    a = a + 1;
    
    // main.js
    const a = require('./a');
    console.log(a); // 2
    

    a.js导出了一个基础数据类型a,module.exports ,这时 a = 2,后面的 a 再次加一,并不会影响前面a的结果。

    例子2
    // b.js
    let b = {
        x: 1,
        y: 2,
    };
    b.x = 3;
    module.exports = b;
    b.x = 4;
    
    // main.js
    const b = require('./b');
    console.log(a.x); // 4
    

    当a不是基础的数据类型,而是一个引用类型时,module.exports 后面的赋值 a.x = 4 生效。并且你可以在外部改变这个值。

    // main.js
    const b = require('./b');
    console.log(b.x); // 4
    b.x= 5;
    console.log(b.x); // 5
    
    当基础数据类型和引用数据类型出现不一致的结果

    reqire时会从头到尾先运行目标文件,当 a 是基本数据类型时,运行到 module.exports 时将当前 a 的值赋值给 module.exports ,也就相当于在内存中创建了一个变量然后给它赋值为2,它的值就不会改变了,后面对 a 的修改并不会对它有影响。

    当为引用数据类型时,赋值给 module.exports 是一个内存地址指向对象,后续对对象的修改也会反应到 module.exports 中。

    模块被加载之后会被缓存起来


    运行上面的列1、列2,在 main.js 中输出 rquire.cache 。 Node 模块机制不完全指北

    模块基于其文件名进行缓存(绝对路径),由于调用模块的位置的不同,模块可能被解析成不同的文件名(比如从 node_modules 目录加载),这样就不能保证 require('xx') 总能返回完全相同的对象。

    此外,在不区分大小写的文件系统或操作系统中,被解析成不同的文件名可以指向同一文件,但缓存仍然会将它们视为不同的模块,并多次重新加载。 例如, require('./a') 和 require('./A') 返回两个不同的对象,而不会管 ./a 和 ./A 是否是相同的文件。

    模块的循环引用


    // a.js
    console.log('a starting');
    exports.done = false;
    const b = require('./b.js');
    console.log('in a, b.done = %j', b.done);
    exports.done = true;
    console.log('a done');
    
    // b.js
    console.log('b starting');
    exports.done = false;
    const a = require('./a.js');
    console.log('in b, a.done = %j', a.done);
    exports.done = true;
    console.log('b done');
    
    // main.js
    console.log('main starting');
    const a = require('./a.js');
    const b = require('./b.js');
    console.log('in main, a.done = %j, b.done = %j', a.done, b.done);
    
    // 运行结果
    $ node main.js
    main starting
    a starting
    b starting
    in b, a.done = false
    b done
    in a, b.done = true
    a done
    in main, a.done = true, b.done = true
    
    1. node main.js
    2. require a.js,load a.js,输出“a starting“
    3. a: exports.done = false,require b.js,load b.js
    4. 输出”b starting“,b: exports.done = false
    5. require a.js, 由于a.js没有执行完,将未完成的副本导出,所以 a = {done: false}
    6. 输出in b, a.done = false
    7. b: exports.done = true,输出b done,b.js执行完毕,返回a.js继续执行
    8. b = { done: true },输出in a, b.done = true,输出a done
    9. a.js 执行完毕,a = { done: true } ,返回 main.js 继续执行,require b.js
    10. 由于 b.js 已经被执行完毕,缓存中拿值,现在 a = { done: true }, b = { done: true }
    11. 输出in main, a.done = true, b.done = true

    循环依赖的时候为什么不会无限循环引用


    源码解析:有兴趣的同学可以看看这里

    不会循环引用的大体思路是:

    1. require('./a.js');此时会调用 self.require(), 然后会走到module._load,在_load中会判断./a.js是否被load过,当然运行到这里,./a.js还没被 load 过,所以会走完整个load流程,直到_compile。
    2. 运行./a.js,运行到 exports.done = false 的时候,给 esports 增加了一个属性。此时的 exports={done: false}。
    3. 运行require('./b.js'),同 第 1 步。
    4. 运行./b.js,到require('./a.js')。此时走到_load函数的时候发现./a.js已经被load过了,所以会直接从_cache中返回。所以此时./a.js还没有运行完,exports = {done.false},那么返回的结果就是 in b, a.done = false;
    5. ./b.js全部运行完毕,回到./a.js中,继续向下运行,此时的./b.js的 exports={done:true}, 结果自然是in main, a.done=true, b.done=true

    未定义变量引用问题

    // a.js
    let a = 1;
    a = a + 1;
    x = 10;
    module.exports = a;
    
    // b.js
    let b = {
      x: 1,
      y: 2,
    };
    b.x = 3;
    console.log(x); // 10
    module.exports = b;
    
    // main.js
    const a = require('./a');
    console.log(a); // 2
    
    const b = require('./b');
    console.log(b.x); // 4
    

    发现在 b.js 总可以访问到 a.js 未定义的变量 x。还可以正常获取到值。原因很简单,因为 x 是一个未声明的变量,也就是一个挂在全局的变量,那么在其他地方当然是可以拿到的。

    exports和module.exports的区别


    在 node 的 js 模块里可以直接调用 exports 和 module 两个“全局”变量,但是 exports 是 module.exports 的一个引用。在 node 编译的过程中,会把js模块编译成如下形式:

    // require 是对 Node.js 实现查找模块的 Module._load 实例的引用
    // __finename 和 __dirname 是 Node.js 在查找该模块后找到的模块名称和模块绝对路径
    (function(exports, require, module, __filename, __dirname){
      // ....
    })
    
    • exports 是 module.exports 的一个引用
    • module.exports 初始化是一个{},exports 也只想这个{}
    • require 引用返回的是 module.exports,而不是 exports
    • exports.xxx = xxxx 相当于在导出对象上直接添加属性或者修改属性值,在调用模块直接可见
    • exports = xxx 为 exports 重新分配内存,将脱离 module.exports ,两者无关联。调用模块将不能访问。

    参考


    • cnodejs.org/topic/56710…
    • www.bookstack.cn/read/Nodejs…

    起源地 » Node 模块机制不完全指北

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元