最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 深入理解CommonJS

    正文概述 掘金(忘尘。)   2020-12-13   255

    深入理解CommonJS

    参考

    • Node.js Design Patterns

    IIFE

    在JS没有模块化之前,我们只能通过块级作用域对数据进行封装,最常见的方式就是使用:立即执行函数(IIFE)

    const myModule = (() => {
      const privateFoo = () => {};
      const privateBar = [];
      const exported = {
        publicFoo: () => {},
        publicBar: () => {},
      };
      return exported;
    })(); // once the parenthesis here are parsed, the function will be invoked
    
    console.log(myModule);
    console.log(myModule.privateFoo, myModule.privateBar);
    
    
    { publicFoo: [Function: publicFoo],
      publicBar: [Function: publicBar] }
    undefined undefined
    

    CommonJS modules

    • 通过require函数进行导入
    • 通过export或者module.exports导出
    • 同步加载

    模块加载

    在讲导入和导出之前,我们先来讨论一下被导入和导出的代码是怎么加载的。

    function loadModule(filename, module, require) {
      const wrappedSrc = `(function (module, exports, require) { 
          ${fs.readFileSync(filename, "utf8")}
          })(module, module.exports, require)`;
      eval(wrappedSrc);
    }
    
    
    1. 这里和上面我们说的IFFE唯一的区别就是,我们给执行函数提供了以下三个参数
      • module
      • exports:我们看传参会发现,它实际上等于module.exports
      • require
    1. 读取文件使用的是fs.readFileSync,是使用同步读取的。

    require方法

    从上面的代码中,我们原本的JS文件就会被包裹在一个函数中执行,在文档中这个函数称为 The module wrapper。那么文件在执行的时候,就可以拥有该函数传递进来的参数。

    // 1.js
    console.log(1)
    

    假设这个文件被加载了,最后执行的代码就会是以下代码

    (function (module, exports, require) {
      //通过文件读取 加载进来的 1.js文件 中的代码
      console.log(1)
    })(module, module.exports, require);
    
    

    所以,如果我们是一个被加载的文件,我们就可以使用以下两个关键词

    • require:通过该函数加载本地文件从而导入新的模块内容
    • exportsmodule.exports:导出当前模块中的值

    我们先来写一个常用的模块代码

    // load another dependency
    const dependency = require('./anotherModule')
    // a private function
    function log() {
      console.log(`Well done ${dependency.username}`)
    }
    // the API to be exported for public use
    module.exports.run = () => {
      log()
    }
    

    就像我们在IIFE中借助块级作用域进行封装,通过the module wrapper,模块中的值除非挂载在module.exports上,否则模块里的任一值将是私有的。

    require实现

    function require(moduleName) {
      console.log(`Require invoked for module: ${moduleName}`);
      const id = require.resolve(moduleName);
      if (require.cache[id]) {
        return require.cache[id].exports;
      }
      // module metadata
      const module = {
        exports: {},
        id,
      };
      // Update the cache
      require.cache[id] = module;
      // load the module
      loadModule(id, module, require);
      // return exported variables
      return module.exports;
    }
    require.cache = {};
    require.resolve = (moduleName) => {
      /* resolve a full module id from the moduleName */
    };
    
    

    补全路径名 && id && resulve

    function require(moduleName) {
      // ....
      const id = require.resolve(moduleName); 
      // ....
    }
    // ....
    require.resolve = (moduleName) => {
      /* resolve a full module id from the moduleName */
    };
    
    

    我们拿到moduleName会对该模块的路径通过require.resolve进行补全,得到一个绝对路径。这里的路径补全有一套特定的逻辑,等到后面再讲。

    缓存模块 && module&& cache

    function require(moduleName) {
      // ...
      const module = {
        exports: {},
        id,
      };
      require.cache[id] = module;
      // ...
    }
    require.cache = {};
    // ...
    
    

    创建一个模块对象,对象就两个属性

    • exports:空对象
    • id:前面通过resulve解析出来的路径

    module挂载到requre.cache上,后面就可以通过id作为key来进行查询。

    既然有缓存的概念,那么在创建模块前,我们就可以先判断该模块是否创建,如果有就不可以跳过创建模块的过程,就有

    function require(moduleName) {
      // ...
      if (require.cache[id]) {
        return require.cache[id].exports;
      }
      // 后面创建模块的过程就可以不用执行了
      const module = {
        exports: {},
        id,
      };
      // ...
    }
    // ...
    
    

    加载模块 && loadModule

    function require(moduleName) {
      const id = require.resolve(moduleName);
      const module = {
        exports: {},
        id,
      };
      // 加载模块
      loadModule(id, module, require);
    }
    
    function loadModule(filename, module, require) {
      const wrappedSrc = `(function (module, exports, require) { 
              ${fs.readFileSync(filename, "utf8")}
              })(module, module.exports, require)`;
      eval(wrappedSrc);
    }
    
    

    loadModule就是我们前面实现的过的了,把代码整合起来

    function require(moduleName) {
      const id = require.resolve(moduleName);
      const module = {
        exports: {},
        id,
      };
      // 加载模块
      (function (filename, module, require) {
        (function (module, exports, require) {
          fs.readFileSync(filename, "utf8");
        })(module, module.exports, require);
      })(id, module, require);
    }
    
    

    嵌了那么多函数,实际作用只是把加载进来的文件执行了,然后把我们一开始创建的moudle对象传进去,让执行的代码修改moudle,比如我们前面写的使用代码

    // load another dependency
    const dependency = require('./anotherModule')
    // a private function
    function log() {
      console.log(`Well done ${dependency.username}`)
    }
    // the API to be exported for public use
    module.exports.run = () => {
      log()
    }
    

    最后就会把run挂在到moudle.exports,而这个moudle就是我们前面通过以下代码创建的

    const module = {
      exports: {},
      id,
    };
    

    导出数据 && module.exports

    function require(moduleName) {
      loadModule(id, module, require);
      //   模块加载后,module.exports 已经被加载进来的代码挂在了需要暴露的属性了
      return module.exports;
    }
    require.cache = {};
    

    除了导出外,我们在前面还说过这些模块都会缓存到require上。

    moudle.exportsexports使用方式及差别

    我们在上面知道,这两个变量是通过函数参数传递进来的,那我们在使用moudleexports时,就可以想象着把当前文件的代码,套入到以下代码中:

    const initModule = {
      exports: {
        defatName: "wcdaren",
      },
    };
    
    // 等于 const ret = reuire('./xxx')
    const ret = (function fn(module, exports) {
      // 把JS文件中的代码放在这里
      return module.exports;
    })(initModule, initModule.exports);
    
    
    console.log(`==============>ret`);
    console.log(ret);
    
    

    加载策略resolve

    function require(moduleName) {
      // ...
      const id = require.resolve(moduleName);
      // ...
    }
    // ...
    require.resolve = (moduleName) => {
      /* resolve a full module id from the moduleName */
    };
    
    

    在前面我们已经在了resolverequire的属性方法。具体的作用就是把引用传递进来的路径进行补全得到一个绝对路径的字符串。

    实际项目中,我们最常使用的就是

    • 导入自己写的文件模块
    • 导入node_modules里的包模块
    • 导入node提供的核心模块

    我们把加载策略只按上面三个为所有条件的话,那我们可以简单地概括下,加载策略顺序为:

    1. 判断是否为核心模块,在node本身提供的模块列表中进行查找,如果是就直接返回
    2. 判断是 moduleName是否以/或者./开头,如果是就统一转换成绝对路径进行加载后返回
    3. 如果前两步都没找到,就当做是包模块,到最近的node_moudles来找

    这里只得注意的还要就是关于扩展名的问题,假设我们这么加载

    const dependency = require('./anotherModule')
    

    剔除路径,设文件名anotherModuleX,那么Node在加载的时候会进行以下判断

    X如果是一个文件的话,就直接加载,不然就依次使用以下格式进行加载

    1. X.js
    2. X.json
    3. X.node
    4. X/index.js
    5. X/index.json
    6. X/index.node

    如果是包模块的话,就会先在package.json文件中以main属性中的值,进行上面的扩展名操作

    深入理解CommonJS

    缓存

    function require(moduleName) {
      // ...
      if (require.cache[id]) {
        return require.cache[id].exports;
      }
    
      const module = {
        exports: {},
        id,
      };
      // ...
      // load the module
      loadModule(id, module, require);
      // ...
      return module.exports;
    }
    // ...
    
    

    从我们require的实现中,我们可以发现,一旦模块被加载过后,再次加载时,我们只是返回第一次执行后返回的数据。于是我们可以得出一个结果,模块只会被执行一次。

    // a.js 
    console.log(`==============>111`)
    console.log(111)
    
    
    // main.js
    require("./a");
    require("./a");
    require("./a");
    require("./a");
    require("./a");
    
    

    最终控制台只会打出一次

    ==============>111
    111
    

    循环引用

    Cycles

    我们举个循环引用的例子:

    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);
    console.log('main ending');
    

    打印出来的结果:

    $ 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
    

    深入理解CommonJS

    Title: test
    Note right of main.js: console.log('main starting')
    main.js->a.js: require('./a.js')
    Note right of a.js: console.log('a starting');
    Note right of a.js: exports.done = false;
    a.js -> b.js:  require('./b.js')
    Note right of b.js: console.log('b starting');
    Note right of b.js: exports.done = false;
    b.js -> a.js: require('./a.js')
    a.js->b.js:,
    Note right of b.js:exports.done = true;
    Note right of b.js:console.log('b done');
    b.js->a.js:,
    Note right of a.js: exports.done = true;
    Note right of a.js: console.log('a done');
    a.js->main.js:,
    main.js->b.js:require('./b.js');
    b.js->main.js:,
    Note right of main.js:console.log('main ending');
    
    
    

    When main.js loads a.js, then a.js in turn loads b.js. At that point, b.js tries to load a.js. In order to prevent an infinite loop, an unfinished copy of the a.js exports object is returned to the b.js module. b.js then finishes loading, and its exports object is provided to the a.js module.

    根据文档及流程图,我们知道当发生循环引用时的处理方式是,返回还未全部执行完时的代码,在第一次加载时创建的module.exports对象返回,这就会导致,被引入的模块代码是未完成的。当这个模块的加载位置进行移动时,得到的结果就存在很多不确定性。所以在实际项目中我们应该避免这种循环引用,比如

    • a依赖于b
    • b依赖于a

    那这种情况就是a和b中有重复需要的代码,我们就应该把这些代码提取出来放在c中,就有

    • a依赖于c
    • b依赖于c

    起源地下载网 » 深入理解CommonJS

    常见问题FAQ

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

    发表评论

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

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

    联系作者

    请选择支付方式

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