原文做者:小美娜娜
连接:点我javascript
Common这个英文单词的意思,相信你们都认识,我记得有一个词组common knowledge是常识的意思,那么CommonJS是否是也是相似于常识性的,你们都理解的意思呢?很明显不是,这个常识一点都不常识。我最初认为commonJS是一个开源的JS库,就是那种很是方便用的库,里面都是一些经常使用的前端方法,然而我错得离谱,CommonJS不只不是一个库,仍是一个看不见摸不着的东西,他只是一个规范!就像校纪校规同样,用来规范JS编程,束缚住前端们。就和Promise同样是一个规范,虽然有许多实现这些规范的开源库,可是这个规范也是能够依靠咱们的JS能力实现的。html
那么CommonJS规范了些什么呢?要解释这个规范,就要从JS的特性提及了。JS是一种直译式脚本语言,也就是一边编译一边运行,因此没有模块的概念。所以CommonJS是为了完善JS在这方面的缺失而存在的一种规范。前端
CommonJS定义了两个主要概念:java
require
函数,用于导入模块module.exports
变量,用于导出模块然而这两个关键字,浏览器都不支持,因此我认为这是为何浏览器不支持CommonJS的缘由。若是必定腰在浏览器上使用CommonJs,那么就须要一些编译库,好比browserify来帮助哦咱们将CommonJs编译成浏览器支持的语法,其实就是实现require和exports。node
那么CommonJS能够用于那些方面呢?虽然CommonJS不能再浏览器中直接使用,可是nodejs能够基于CommonJS规范而实现的,亲儿子的感受。在nodejs中咱们就能够直接使用require和exports这两个关键词来实现模块的导入和导出。es6
require
导入,代码很简单,let {count,addCount}=require("./utils")
就能够了。那么在导入的时候发生了些什么呢??首先确定是解析路径,系统给咱们解析出一个绝对路径,咱们写的相对对路径是给咱们看的,绝对路径是给系统看的,毕竟绝对路径辣么长,看着很费力,尤为是当咱们的的项目在N个文件夹之下的时候。因此requir
e第一件事就是解析路径。咱们能够写的很简洁,只须要写出相对路径和文件名便可,连后缀均可以省略,让require
帮咱们去匹配去寻找。也就是说require
的第一步是解析路径获取到模块内容:编程
fs
,就直接返回模块/
,./
等等,则拼接出一个绝对路径,而后先读取缓存require.cache
再读取文件。若是没有加后缀,则自动加后缀而后一一识别。
.js
解析为JavaScript 文本文件.json
解析JSON对象.node
解析为二进制插件模块require.cache
之中,因此屡次加载require
,获得的对象是同一个。(function(exports, require, module, __filename, __dirname) { // 模块的代码实际上在这里 });
(function(exports, require, module, __filename, __dirname) { // 模块的代码实际上在这里 });
module
说完了require作了些什么事,那么require
触发的module
作了些什么呢?咱们看看用法,先写一个简单的导出模块,写好了模块以后,只须要把须要导出的参数,加入module.exports
就能够了。api
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打断点:
根据这个断点,咱们能够整理出:
黄色圈出来的时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的模块加载,已经有浏览器支持了这个特性,所以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模块导入的几个问题:
x
已经导入了,就不能再从utils中导入x
import
中执行。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模块只能运行再支持此规范的环境之中,nodejs是基于CommonJS规范开发的,所以能够很完美地运行CommonJS模块,而后nodejs不支持ES6的模块规范,因此nodejs的服务器开发你们通常使用CommonJS规范来写。
CommonJS模块导入用require
,导出用module.exports
。导出的对象需注意,若是是静态值,并且很是量,后期可能会有所改动的,请使用函数动态获取,不然没法获取修改值。导入的参数,是能够随意改动的,因此你们使用时要当心。