读懂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 相关文章推荐
ExtJS 2.0实用简明教程 之Border区域布局
Apr 29 Javascript
jquery中常用的函数和属性详细解析
Mar 07 Javascript
JS弹出可拖拽可关闭的div层完整实例
Feb 13 Javascript
jQuery与Ajax以及序列化
Feb 01 Javascript
AngularJS 自定义过滤器详解及实例代码
Sep 14 Javascript
DOM 事件的深入浅出(二)
Dec 05 Javascript
vuejs父子组件通信的问题
Jan 11 Javascript
关于Node.js的events.EventEmitter用法介绍
Apr 01 Javascript
JavaScript正则表达式的贪婪匹配和非贪婪匹配
Sep 05 Javascript
JavaScript实现求最大公共子串的方法
Feb 03 Javascript
layui 富文本赋值,取值,取纯文本值的实例
Sep 18 Javascript
VUEX 数据持久化,刷新后重新获取的例子
Nov 12 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模拟socket一次连接,多次发送数据的实现代码
2011/07/26 PHP
PHP文件上传判断file是否己选择上传文件的方法
2014/11/10 PHP
Joomla语言翻译类Jtext用法分析
2016/05/05 PHP
PHP7常量数组用法分析
2016/09/26 PHP
php双层循环(九九乘法表)
2017/10/23 PHP
关于PHP5.6+版本“No input file specified”问题的解决
2019/12/11 PHP
php封装实现钉钉机器人报警接口的示例代码
2020/08/08 PHP
javascript右下角弹层及自动隐藏(自己编写)
2013/11/20 Javascript
jQuery判断checkbox(复选框)是否被选中以及全选、反选实现代码
2014/02/21 Javascript
js由下向上不断上升冒气泡效果实例
2015/05/07 Javascript
浅谈JavaScript 执行环境、作用域及垃圾回收
2016/05/31 Javascript
angularJS模态框$modal实例代码
2017/05/27 Javascript
深入讲解xhr(XMLHttpRequest)/jsonp请求之abort
2017/07/26 Javascript
浅谈Vuejs中nextTick()异步更新队列源码解析
2017/12/31 Javascript
Express的HTTP重定向到HTTPS的方法
2018/06/06 Javascript
解决vue中虚拟dom,无法实时更新的问题
2018/09/15 Javascript
详解微信小程序中组件通讯
2018/10/30 Javascript
浅析JavaScript异步代码优化
2019/03/18 Javascript
一些可能会用到的Node.js面试题
2019/06/15 Javascript
JQuery+Bootstrap 自定义全屏Loading插件的示例demo
2019/07/03 jQuery
vue-cli点击实现全屏功能
2020/03/07 Javascript
python计算N天之后日期的方法
2015/03/31 Python
Python Json模块中dumps、loads、dump、load函数介绍
2018/05/15 Python
Python DataFrame 设置输出不显示index(索引)值的方法
2018/06/07 Python
Python数据抓取爬虫代理防封IP方法
2018/12/23 Python
pytorch构建多模型实例
2020/01/15 Python
Python实现打包成库供别的模块调用
2020/07/13 Python
python压包的概念及实例详解
2021/02/17 Python
详解html5页面 rem 布局适配方法
2018/01/12 HTML / CSS
One.com挪威:北欧成长最快的网络托管公司
2016/11/19 全球购物
阿拉伯时尚购物网站:Nisnass
2021/02/07 全球购物
师范生实习的个人自我鉴定
2013/10/20 职场文书
专科应届生求职信
2013/11/24 职场文书
针对吵架老公保证书
2015/05/08 职场文书
2015年中秋节主持词
2015/07/30 职场文书
利用Python读取微信朋友圈的多种方法总结
2021/08/23 Python