读懂CommonJS的模块加载


Posted in Javascript onApril 19, 2019

叨叨一会CommonJS

Common这个英文单词的意思,相信大家都认识,我记得有一个词组common knowledge是常识的意思,那么CommonJS是不是也是类似于常识性的,大家都理解的意思呢?很明显不是,这个常识一点都不常识。我最初认为commonJS是一个开源的JS库,就是那种非常方便用的库,里面都是一些常用的前端方法,然而我错得离谱,CommonJS不仅不是一个库,还是一个看不见摸不着的东西,他只是一个规范!就像校纪校规一样,用来规范JS编程,束缚住前端们。就和Promise一样是一个规范,虽然有许多实现这些规范的开源库,但是这个规范也是可以依靠我们的JS能力实现的。

CommonJs规范

那么CommonJS规范了些什么呢?要解释这个规范,就要从JS的特性说起了。JS是一种直译式脚本语言,也就是一边编译一边运行,所以没有模块的概念。因此CommonJS是为了完善JS在这方面的缺失而存在的一种规范。

CommonJS定义了两个主要概念:

  1. require函数,用于导入模块
  2. module.exports变量,用于导出模块

然而这两个关键字,浏览器都不支持,所以我认为这是为什么浏览器不支持CommonJS的原因。如果一定腰在浏览器上使用CommonJs,那么就需要一些编译库,比如browserify来帮助哦我们将CommonJs编译成浏览器支持的语法,其实就是实现require和exports。

那么CommonJS可以用于那些方面呢?虽然CommonJS不能再浏览器中直接使用,但是nodejs可以基于CommonJS规范而实现的,亲儿子的感觉。在nodejs中我们就可以直接使用require和exports这两个关键词来实现模块的导入和导出。

Nodejs中CommomJS模块的实现

require

导入,代码很简单,let {count,addCount}=require("./utils")就可以了。那么在导入的时候发生了些什么呢??首先肯定是解析路径,系统给我们解析出一个绝对路径,我们写的相对对路径是给我们看的,绝对路径是给系统看的,毕竟绝对路径辣么长,看着很费力,尤其是当我们的的项目在N个文件夹之下的时候。所以require第一件事就是解析路径。我们可以写的很简洁,只需要写出相对路径和文件名即可,连后缀都可以省略,让require帮我们去匹配去寻找。也就是说require的第一步是解析路径获取到模块内容:

如果是核心模块,比如fs,就直接返回模块

如果是带有路径的如/,./等等,则拼接出一个绝对路径,然后先读取缓存require.cache再读取文件。如果没有加后缀,则自动加后缀然后一一识别。

  1. .js 解析为JavaScript 文本文件
  2. .json解析JSON对象
  3. .node解析为二进制插件模块

首次加载后的模块会缓存在require.cache之中,所以多次加载require,得到的对象是同一个。

在执行模块代码的时候,会将模块包装成如下模式,以便于作用域在模块范围之内。

(function(exports, require, module, __filename, __dirname) {
// 模块的代码实际上在这里
});

nodejs官方给出的解释,大家可以参考下

module

说完了require做了些什么事,那么require触发的module做了些什么呢?我们看看用法,先写一个简单的导出模块,写好了模块之后,只需要把需要导出的参数,加入module.exports就可以了。

 

let count=0
function addCount(){
  count++
}
module.exports={count,addCount}

然后根据require执行代码时需要加上的,那么实际上我们的代码长成这样:

(function(exports, require, module, __filename, __dirname) {
  let count=0
  function addCount(){
    count++
  }
  module.exports={count,addCount}
});

require的时候究竟module发生了什么,我们可以在vscode打断点:

读懂CommonJS的模块加载

根据这个断点,我们可以整理出:

黄色圈出来的时require,也就是我们调用的方法

红色圈出来的时Module的工作内容

Module._compile
Module.extesions..js
Module.load
tryMouduleLoad
Module._load
Module.runMain

蓝色圈出来的是nodejs干的事,也就是NativeModule,用于执行module对象的。

我们都知道在JS中,函数的调用时栈stack的方式,也就是先近后出,也就是说require这个函数触发之后,图中的运行时从下到上运行的。也就是蓝色框最先运行。我把他的部分代码扒出来,研究研究。

NativeModule原生代码关键代码,这一块用于封装模块的。

NativeModule.wrap = function(script) {
  return NativeModule.wrapper[0] + script + NativeModule.wrapper[1];
};

NativeModule.wrapper = [
  '(function (exports, require, module, __filename, __dirname) { ',
  '\n});'
];

等NativeModule触发Module.runMain之后,我们的模块加载开始了,我们按照从下至上的顺序来解读吧。

Module._load,就是新建一个module对象,然后将这个新对象放入Module缓存之中。

var module = new Module(filename, parent);
Module._cache[filename] = module;

tryMouduleLoad,然后就是新建的module对象开始解析导入的模块内容

module.load(filename);

新建的module对象继承了Module.load,这个方法就是解析文件的类型,然后分门别类地执行

Module.extesions..js这就干了两件事,读取文件,然后准备编译

Module._compile终于到了编译的环节,那么JS怎么运行文本?将文本变成可执行对象,js有3种方法:

eval方法eval("console.log('aaa')")

new Function() 模板引擎

let str="console.log(a)"
new Function("aaa",str)

node执行字符串,我们用高级的vm

let vm=require("vm")
let a='console.log("a")'
vm.runInThisContext(a)

这里Module用vm的方式编译,首先是封装一下,然后再执行,最后返回给require,我们就可以获得执行的结果了。

var wrapper = Module.wrap(content);
var compiledWrapper = vm.runInThisContext(wrapper, {
  filename: filename,
  lineOffset: 0,
  displayErrors: true
});

因为所有的模块都是封装之后再执行的,也就说导入的这个模块,我们只能根据module.exports这一个对外接口来访问内容。

总结一下

这些代码看的人真的很晕,其实主要流程就是require之后解析路径,然后触发Module这一个类,然后Module的_load的方法就是在当前模块中创建一个新module的缓存,以保证下一次再require的时候可以直接返回而不用再次执行。然后就是这个新module的load方法载入并通过VM执行代码返回对象给require。

正因为是这样编译运行之后赋值给的缓存,所以如果export的值是一个参数,而不是函数,那么如果当前参数的数值改变并不会引起export的改变,因为这个赋予export的参数是静态的,并不会引起二次运行。

CommonJs模块和ES6模块的区别

使用场景

CommonJS因为关键字的局限性,因此大多用于服务器端。而ES6的模块加载,已经有浏览器支持了这个特性,因此ES6可以用于浏览器,如果遇到不支持ES6语法的浏览器,可以选择转译成ES5。

语法差异

ES6也是一种JavaScript的规范,它和CommonJs模块的区别,显而易见,首先代码就不一样,ES6的导入导出很直观import和export。

commonJS ES6
支持的关键字 arguments,require,module,exports,__filename,__dirname import,export
导入 const path=require("path") import path from "path"
导出 module.exports = APP; export default APP
导入的对象 随意修改 不能随意修改
导入次数 可以随意require,但是除了第一次,之后都是从模块缓存中取得 在头部导入

** 大家注意了!划重点!nodejs是CommonJS的亲儿子,所以有些ES6的特性并不支持,比如ES6对于模块的关键字import和export,如果大家在nodejs环境下运行,就等着大红的报错吧~**

加载差异

除了语法上的差异,他们引用的模块性质是不一样的。虽然都是模块,但是这模块的结构差异很大。

在ES6中,如果大家想要在浏览器中测试,可以用以下代码:

//utils.js
const x = 1;
export default x
<script type="module">
  import x from './utils.js';
  console.log(x);
  export default x
</script>

首先要给script一个type="module"表明这里面是ES6的模块,而且这个标签默认是异步加载,也就是页面全部加载完成之后再执行,没有这个标签的话代码不然无法运行哦。然后就可以直接写import和export了。

ES6模块导入的几个问题:

  1. 相同的模块只能引入一次,比如x已经导入了,就不能再从utils中导入x
  2. 不同的模块引入相同的模块,这个模块只会在首次import中执行。
  3. 引入的模块就是一个值的引用,并且是动态的,改变之后其他的相关值也会变化
  4. 引入的对象不可随意斩断链接,比如我引入的count我就不能修改他的值,因为这个是导入进来的,想要修改只能在count所在的模块修改。但是如果count是一个对象,那么可以改变对象的属性,比如count.one=1,但是不可以count={one:1}。

大家可以看这个例子,我写了一个改变object值的小测试,大家会发现utils.js中的count初始值应该是0,但是运行了addCount所以count的值动态变化了,因此count的值变成了2。

let count=0
function addCount(){
  count=count+2
}
export {count,addCount}
<script type="module">
  import {count,addCount} from './utils.js';
  //count=4//不可修改,会报错
  addCount()
  console.log(count);
</script>

与之对比的是commonJS的模块引用,他的特性是:

上一节已经解释了,模块导出的固定值就是固定值,不会因为后期的修改而改变,除非不导出静态值,而改成函数,每次调用都去动态调用,那么每次值都是最新的了。
导入的对象可以随意修改,相当于只是导入模块中的一个副本。

如果想要深入研究,大家可以参考下阮老师的ES6入门——Module 的加载实现。

CommonJS模块总结

CommonJS模块只能运行再支持此规范的环境之中,nodejs是基于CommonJS规范开发的,因此可以很完美地运行CommonJS模块,然后nodejs不支持ES6的模块规范,所以nodejs的服务器开发大家一般使用CommonJS规范来写。

CommonJS模块导入用require,导出用module.exports。导出的对象需注意,如果是静态值,而且非常量,后期可能会有所改动的,请使用函数动态获取,否则无法获取修改值。导入的参数,是可以随意改动的,所以大家使用时要小心。

以上所述是小编给大家介绍的CommonJS的模块加载详解整合,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对三水点靠木网站的支持!

Javascript 相关文章推荐
浏览器的JavaScript引擎的识别方法
Oct 20 Javascript
js中settimeout方法加参数的使用实例
Feb 27 Javascript
JS+CSS实现简单滑动门(滑动菜单)效果
Sep 19 Javascript
jquery封装插件时匿名函数形参和实参的写法解释
Feb 14 Javascript
基于vue的换肤功能的示例代码
Oct 10 Javascript
webpack构建换肤功能的思路详解
Nov 27 Javascript
基于 D3.js 绘制动态进度条的实例详解
Feb 26 Javascript
react 国际化的实现代码示例
Sep 14 Javascript
值得收藏的八个常用的js正则表达式
Oct 19 Javascript
详解vue中使用axios对同一个接口连续请求导致返回数据混乱的问题
Nov 06 Javascript
JS 事件机制完整示例分析
Jan 15 Javascript
Echarts在Taro微信小程序开发中的踩坑记录
Nov 09 Javascript
js module大战
Apr 19 #Javascript
如何根据业务封装自己的功能组件
Apr 19 #Javascript
vue项目打包上传github并制作预览链接(pages)
Apr 19 #Javascript
vue组件之间的数据传递方法详解
Apr 19 #Javascript
详解keep-alive + vuex 让缓存的页面灵活起来
Apr 19 #Javascript
一个Java程序猿眼中的前后端分离以及Vue.js入门(推荐)
Apr 19 #Javascript
基于javascript的拖拽类封装详解
Apr 19 #Javascript
You might like
松下Panasonic RF-B65电路分析
2021/03/02 无线电
浅析echo(),print(),print_r(),return之间的区别
2013/11/27 PHP
php中有关合并某一字段键值相同的数组合并的改进
2015/03/10 PHP
thinkPHP3.2.3实现阿里大于短信验证的方法
2018/06/06 PHP
PHP实现的敏感词过滤方法示例
2019/03/06 PHP
laravel 使用事件系统统计浏览量的实现
2019/10/16 PHP
Extjs中ComboBox加载并赋初值的实现方法
2012/03/22 Javascript
jquery动态加载js三种方法实例
2013/08/03 Javascript
IE8下Jquery获取select选中的值post到后台报错问题
2014/07/02 Javascript
浅谈JavaScript中定义变量时有无var声明的区别
2014/08/18 Javascript
jQuery ajax请求返回list数据动态生成input标签,并把list数据赋值到input标签
2016/03/29 Javascript
JSON 的正确用法探讨:Pyhong、MongoDB、JavaScript与Ajax
2016/05/15 Javascript
etmvc+jQuery EasyUI+combobox多值操作实现角色授权实例
2016/11/09 Javascript
微信小程序开发之实现选项卡(窗口顶部TabBar)页面切换
2016/11/25 Javascript
vue-dialog的弹出层组件
2020/05/25 Javascript
微信小程序使用map组件实现路线规划功能示例
2019/01/22 Javascript
Vue+Node服务器查询Mongo数据库及页面数据传递操作实例分析
2019/12/20 Javascript
js实现div色块拖动录制
2020/01/16 Javascript
Javascript数组及类数组相关原理详解
2020/10/29 Javascript
Vue ​v-model相关知识总结
2021/01/28 Vue.js
基于vue-simple-uploader封装文件分片上传、秒传及断点续传的全局上传插件功能
2021/02/23 Vue.js
python元组操作实例解析
2014/09/23 Python
Python查找最长不包含重复字符的子字符串算法示例
2019/02/13 Python
python实现扫雷小游戏
2020/04/24 Python
Python使用Matlab命令过程解析
2020/06/04 Python
Sport-Thieme荷兰:购买体育用品
2019/08/25 全球购物
照片礼物和装饰:MyPhoto
2019/11/02 全球购物
MIXIT官网:俄罗斯最大的化妆品公司之一
2020/01/25 全球购物
数控专业大学生的自我鉴定
2013/11/13 职场文书
股东合作协议书范本
2014/04/14 职场文书
记账会计岗位职责
2014/06/16 职场文书
幼儿园大班区域活动总结
2014/07/09 职场文书
优秀班主任推荐材料
2014/12/17 职场文书
部队个人年终总结
2015/03/02 职场文书
2019年励志签名:致拼搏路上的自己
2019/10/11 职场文书
一文了解MYSQL三大范式和表约束
2022/04/03 MySQL