读懂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 06 Javascript
js下判断 iframe 是否加载完成的完美方法
Oct 26 Javascript
js 浏览器事件介绍
Mar 30 Javascript
javascript异步处理工作机制详解
Apr 13 Javascript
js实现iPhone界面风格的单选框和复选框按钮实例
Aug 18 Javascript
XML、HTML、CSS与JS的区别整理
Feb 18 Javascript
js控制div层的叠加简单方法
Oct 15 Javascript
javascript跨域请求包装函数与用法示例
Nov 03 Javascript
JavaScript数据类型和变量_动力节点Java学院整理
Jun 26 Javascript
关于HTML5的data-*自定义属性的总结
May 05 Javascript
Vue 路由间跳转和新开窗口的方式(query、params)
Dec 25 Javascript
在Chrome DevTools中调试JavaScript的实现
Apr 07 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 读取Postgresql中的数组
2013/04/14 PHP
Zend的MVC机制使用分析(二)
2013/05/02 PHP
再推荐十款免费的php开发工具
2015/11/09 PHP
PHP MYSQL简易交互式站点开发
2016/12/27 PHP
如何打开php的gd2库
2017/02/09 PHP
PHP使用Curl实现模拟登录及抓取数据功能示例
2018/04/27 PHP
php+mysql开发中的经验与常识小结
2019/03/25 PHP
YII2框架中behavior行为的理解与使用方法示例
2020/03/13 PHP
19个很有用的 JavaScript库推荐
2011/06/27 Javascript
javascript学习基础笔记之DOM对象操作
2011/11/03 Javascript
JS操作Cookie写入和读取实例代码
2013/10/20 Javascript
微信企业号开发之微信考勤百度地图定位
2015/09/11 Javascript
JavaScript获取浏览器信息的方法
2015/11/20 Javascript
JavaScript继承模式粗探
2016/01/12 Javascript
bootstrap table 多选框分页保留示例代码
2017/03/08 Javascript
JavaScript上传文件时不用刷新页面方法总结(推荐)
2017/08/15 Javascript
vue监听浏览器原生返回按钮,进行路由转跳操作
2020/09/09 Javascript
[03:24]2014DOTA2国际邀请赛 神秘商店生意火爆
2014/07/18 DOTA
python操作mysql中文显示乱码的解决方法
2014/10/11 Python
python中requests和https使用简单示例
2018/01/18 Python
[原创]windows下Anaconda的安装与配置正解(Anaconda入门教程)
2018/04/05 Python
python单线程文件传输的实例(C/S)
2019/02/13 Python
python 多线程重启方法
2019/02/18 Python
python通过http下载文件的方法详解
2019/07/26 Python
python Web flask 视图内容和模板实现代码
2019/08/23 Python
django ajax发送post请求的两种方法
2020/01/05 Python
de Bijenkorf比利时官网:荷兰最知名的百货商店
2017/06/29 全球购物
MCAKE蛋糕官方网站:一直都是巴黎的味道
2018/02/06 全球购物
入党自我评价范文
2014/02/02 职场文书
关于感谢信的范文
2015/01/23 职场文书
六一文艺汇演开幕词
2015/01/29 职场文书
部门经理助理岗位职责
2015/04/13 职场文书
亮剑观后感300字
2015/06/05 职场文书
校长新学期致辞
2015/07/30 职场文书
2016年世界人口日宣传活动总结
2016/04/05 职场文书
广播稿:校园广播稿范文
2019/04/17 职场文书