读懂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 相关文章推荐
js用图作提交按钮或超连接
Mar 26 Javascript
js动态在form上插入enctype=multipart/form-data的问题
May 24 Javascript
jQuery 淡入淡出 png图在ie8下有黑色边框的解决方法
Mar 05 Javascript
jquery中加载图片自适应大小主要实现代码
Aug 23 Javascript
Node.js插件的正确编写方式
Aug 03 Javascript
JavaScript中的方法重载实例
Mar 16 Javascript
Node.js 应用跑得更快 10 个技巧
Apr 03 Javascript
第七篇Bootstrap表单布局实例代码详解(三种表单布局)
Jun 21 Javascript
基于JavaScript实现单选框下拉菜单添加文件效果
Jun 26 Javascript
瀑布流的实现方式(原生js+jquery+css3)
Jun 28 Javascript
使用mpvue搭建一个初始小程序及项目配置方法
Dec 03 Javascript
微信小程序设置全局请求URL及封装wx.request请求操作示例
Apr 02 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
PHP设计模式之结构模式的深入解析
2013/06/13 PHP
ThinkPHP3.1的Widget新用法
2014/06/19 PHP
PHP实现返回JSON和XML的类分享
2015/01/28 PHP
php实现用手机关闭计算机(电脑)的方法
2015/04/22 PHP
在PHP中输出JS语句以及乱码问题的解决方案
2019/02/13 PHP
JavaScript语言中的Literal Syntax特性分析
2007/03/08 Javascript
jQuery在vs2008及js文件中的无智能提示的解决方法
2010/12/30 Javascript
js两行代码按指定格式输出日期时间
2011/10/21 Javascript
JS可以控制样式的名称写法一览
2014/01/16 Javascript
将HTML的左右尖括号等转义成实体形式的两种实现方式
2014/05/04 Javascript
jquery统计用户选中的复选框的个数
2014/06/06 Javascript
JS 新增Cookie 取cookie值 删除cookie 举例详解
2014/10/10 Javascript
Jquery中Event对象属性小结
2015/02/27 Javascript
JS组件系列之Bootstrap table表格组件神器【二、父子表和行列调序】
2016/05/10 Javascript
Vue数据驱动模拟实现2
2017/01/11 Javascript
vue2组件实现懒加载浅析
2017/03/29 Javascript
详解最新vue-cli 2.9.1的webpack存在问题
2017/12/16 Javascript
jQuery选择器之基本过滤选择器用法实例分析
2019/02/19 jQuery
学习node.js 断言的使用详解
2019/03/18 Javascript
Vue.js中的组件系统
2019/05/30 Javascript
Js on及addEventListener原理用法区别解析
2020/07/11 Javascript
python 计算数组中每个数字出现多少次--“Bucket”桶的思想
2017/12/19 Python
python matlibplot绘制3D图形
2018/07/02 Python
django的分页器Paginator 从django中导入类
2019/07/25 Python
python实现飞机大战游戏(pygame版)
2020/10/26 Python
python实现替换word中的关键文字(使用通配符)
2020/02/13 Python
jupyter notebook参数化运行python方式
2020/04/10 Python
css3 中的新特性加强记忆详解
2016/04/16 HTML / CSS
瑞典快乐袜子:Happy Socks
2018/02/16 全球购物
巴基斯坦购物网站:Goto
2019/03/11 全球购物
十佳班主任事迹材料
2014/01/18 职场文书
学生安全责任书模板
2014/07/25 职场文书
门面房租房协议书
2014/12/01 职场文书
2016应届毕业生实习评语
2015/12/01 职场文书
2016年安全月活动总结
2016/04/06 职场文书
golang 实现对Map进行键值自定义排序
2021/04/28 Golang