读懂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 option location 页面跳转实现代码
Dec 27 Javascript
javascript学习笔记(二十) 获得和设置元素的特性(属性)
Jun 20 Javascript
javascript格式化日期时间方法汇总
Jun 19 Javascript
深入理解javascript中的 “this”
Jan 17 Javascript
BootStrap实现鼠标悬停下拉列表功能
Feb 17 Javascript
不到200行 JavaScript 代码实现富文本编辑器的方法
Jan 03 Javascript
axios异步提交表单数据的几种方法
Aug 11 Javascript
mpvue 页面预加载新增preLoad生命周期的两种方式
Oct 17 Javascript
JS实现商城秒杀倒计时功能(动态设置秒杀时间)
Dec 12 Javascript
jQuery实现鼠标移入显示蒙版效果
Jan 11 jQuery
JS实现购物车基本功能
Nov 08 Javascript
JavaScript 中的六种循环方法
Jan 06 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
《五等分的花嫁》漫画完结!2020年10月第2期TV动画制作组换血!
2020/03/06 日漫
php 目录遍历、删除 函数的使用介绍
2013/04/28 PHP
php计算整个目录大小的方法
2015/06/01 PHP
thinkPHP自定义类实现方法详解
2016/11/30 PHP
php、mysql查询当天,查询本周,查询本月的数据实例(字段是时间戳)
2017/02/04 PHP
Stop SQL Server
2007/06/21 Javascript
js 获取浏览器版本以此来调整CSS的样式
2014/06/03 Javascript
JavaScript事件委托实例分析
2015/05/26 Javascript
js实现文本框支持加减运算的方法
2015/08/19 Javascript
初步使用Node连接Mysql数据库
2016/03/03 Javascript
angularjs实现文字上下无缝滚动特效代码
2016/09/04 Javascript
微信小程序 video组件详解
2016/10/25 Javascript
jQuery Validation Engine验证控件调用外部函数验证的方法
2017/01/18 Javascript
JavaScript中为事件指定处理程序的五种方式分析
2018/07/27 Javascript
用vuex写了一个购物车H5页面的示例代码
2018/12/04 Javascript
基于mpvue小程序使用echarts画折线图的方法示例
2019/04/24 Javascript
jquery插件开发模式实例详解
2019/07/20 jQuery
jquery-ui 进度条功能示例【测试可用】
2019/07/25 jQuery
基于vue手写tree插件的那点事儿
2019/08/20 Javascript
python排序方法实例分析
2015/04/30 Python
关于Python面向对象编程的知识点总结
2017/02/14 Python
Python进阶之自定义对象实现切片功能
2019/01/07 Python
对python字典过滤条件的实例详解
2019/01/22 Python
解决py2exe打包后,总是多显示一个DOS黑色窗口的问题
2019/06/21 Python
浅谈keras中Dropout在预测过程中是否仍要起作用
2020/07/09 Python
pycharm 快速解决python代码冲突的问题
2021/01/15 Python
CSS3 Media Queries详细介绍和使用实例
2014/05/08 HTML / CSS
html5写一个BUI折叠菜单插件的实现方法
2019/09/11 HTML / CSS
Canon佳能美国官方商店:购买数码相机、数码单反相机、镜头和打印机
2016/11/15 全球购物
如何设定的weblogic的热启动模式(开发模式)与产品发布模式
2012/09/08 面试题
妇产科护士自我鉴定
2013/10/15 职场文书
历史系自荐信范文
2013/12/24 职场文书
2014年作风建设心得体会
2014/10/22 职场文书
中秋节作文(五年级)之关于月亮
2019/09/11 职场文书
Nginx配置https原理及实现过程详解
2021/03/31 Servers
vue-treeselect的基本用法以及解决点击无法出现拉下菜单
2022/04/30 Vue.js