读懂CommonJS的模块加载
叨叨一会CommonJS
Common这个英文单词的意思,相信大家都认识,我记得有一个词组common knowledge是常识的意思,那么CommonJS是不是也是类似于常识性的,大家都理解的意思呢?很明显不是,这个常识一点都不常识。我最初认为commonJS是一个开源的JS库,就是那种非常方便用的库,里面都是一些常用的前端方法,然而我错得离谱,CommonJS不仅不是一个库,还是一个看不见摸不着的东西,他只是一个规范!就像校纪校规一样,用来规范JS编程,束缚住前端们。就和Promise一样是一个规范,虽然有许多实现这些规范的开源库,但是这个规范也是可以依靠我们的JS能力实现的。
CommonJs规范
那么CommonJS规范了些什么呢?要解释这个规范,就要从JS的特性说起了。JS是一种直译式脚本语言,也就是一边编译一边运行,所以没有模块的概念。因此CommonJS是为了完善JS在这方面的缺失而存在的一种规范。
CommonJS定义了两个主要概念:
require
函数,用于导入模块module.exports
变量,用于导出模块
然而这两个关键字,浏览器都不支持,所以我认为这是为什么浏览器不支持CommonJS的原因。如果一定腰在浏览器上使用CommonJs,那么就需要一些编译库,比如browserify来帮助哦我们将CommonJs编译成浏览器支持的语法,其实就是实现require和exports。
那么CommonJS可以用于那些方面呢?虽然CommonJS不能再浏览器中直接使用,但是nodejs可以基于CommonJS规范而实现的,亲儿子的感觉。在nodejs中我们就可以直接使用require和exports这两个关键词来实现模块的导入和导出。
Nodejs中CommomJS模块的实现
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) {
// 模块的代码实际上在这里
});
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打断点:
根据这个断点,我们可以整理出:
黄色圈出来的时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模块导入的几个问题:
- 相同的模块只能引入一次,比如
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模块总结
CommonJS模块只能运行再支持此规范的环境之中,nodejs是基于CommonJS规范开发的,因此可以很完美地运行CommonJS模块,然后nodejs不支持ES6的模块规范,所以nodejs的服务器开发大家一般使用CommonJS规范来写。
CommonJS模块导入用require
,导出用module.exports
。导出的对象需注意,如果是静态值,而且非常量,后期可能会有所改动的,请使用函数动态获取,否则无法获取修改值。导入的参数,是可以随意改动的,所以大家使用时要小心。
读懂CommonJS的模块加载的更多相关文章
- 【前端】CommonJS的模块加载机制
CommonJS的模块加载机制 CommonJS模块的加载机制是,输入的是被输出的值的拷贝.也就是说,一旦输出一个值,模块内部的变化就影响不到这个值. 例如: // lib.js var counte ...
- 对于模块加载:ES6、CommonJS、AMD、CMD的区别
运行和编译的概念 编译包括编译和链接两步. 编译,把源代码翻译成机器能识别的代码或者某个中间状态的语言. 比如java只有JVM识别的字节码,C#中只有CLR能识别的MSIL.还简单的作一些比如检查有 ...
- Javascript模块化编程之CommonJS,AMD,CMD,UMD模块加载规范详解
JavaSript模块化 在了解AMD,CMD规范前,还是需要先来简单地了解下什么是模块化,模块化开发? 模块化是指在解决某一个复杂问题或者一系列的杂糅问题时,依照一种分类的思维把问 题进行系 ...
- Layui 源码浅读(模块加载原理)
经典开场 // Layui ;! function (win) { var Lay = function () { this.v = '2.5.5'; }; win.layui = new Lay() ...
- node模块加载层级优化
模块加载痛点 大家也或多或少的了解node模块的加载机制,最为粗浅的表述就是依次从当前目录向上级查询node_modules目录,若发现依赖则加载.但是随着应用规模的加大,目录层级越来越深,若是在某个 ...
- ES6模块加载
两种加载方式 加载方式 规范 命令 特点 运行时加载 CommonJS/AMD require 社区方案,提供了服务器/浏览器的模块加载方案 非语言层面的标准 只能在运行时确定模块的依赖关系及输入/输 ...
- 第三章:模块加载系统(requirejs)
任何一门语言在大规模应用阶段,必然要经历拆分模块的过程.便于维护与团队协作,与java走的最近的dojo率先引入加载器,早期的加载器都是同步的,使用document.write与同步Ajax请求实现. ...
- JS模块加载器加载原理是怎么样的?
路人一: 原理一:id即路径 原则.通常我们的入口是这样的: require( [ 'a', 'b' ], callback ) .这里的 'a'.'b' 都是 ModuleId.通过 id 和路径的 ...
- 转: javascript模块加载框架seajs详解
javascript模块加载框架seajs详解 SeaJS是一个遵循commonJS规范的javascript模块加载框架,可以实现javascript的模块化开发和模块化加载(模块可按需加载或全部加 ...
随机推荐
- hibernate的基础学习--一对多关联
基本的用户和部门类,只有uuid和名称,没有其余字段. 配置文件 部门: <?xml version="1.0" encoding="utf-8" ?&g ...
- hdoj1007【几何】【未完待续】
题意: 在一个平面上有n(1e5)个点,然后求一个圆来包住这些点,求这个圆的最小半径. 思考: 要使一个圆直接包了这些点,没有任何思路..
- AGC031 A~C
A题意:给定字符串s,求无重复字符子序列个数(子序列相同位置不同算不同) 在最后加一串a~z表示选了这些就是不选这个字符了,然后答案就是每次选每个字符位置的方案数的积 #include<iost ...
- Typora练习测试
目录 一级标题 二级标题 三级标题 一级标题 二级标题 三级标题 这是下划线 删除线 字体加粗ctrl+b 这是倾斜线 1111 牛奶 面包 鸡蛋 包子 蛋糕 测试 牛奶 面包 鸡蛋 电脑 鼠标 键盘 ...
- C#批量插入Sybase数据库,Anywhere 8
数据库版本是Adaptive Server Anywhere 8 1.添加引用,程序集 iAnywhere.Data.AsaClient.这个和SQLServer的System.Data.SqlCli ...
- adb shell报错:error: insufficient permissions for device的解决办法
1.错误描述 执行 adb shell 时,报错如下; error: insufficient permissions for device 2.解决办法 1,终端执行 lsusb 结果如下,注意绿 ...
- pyton 基础,运算符及字符类型。
一.python运算符: 二.数据类型: 1.数字: int :整型 32位机器上一个整数取值范围为-2**31~2**31-1即-2147483648~2147483647 64位机器上一个整数取 ...
- Nagios安装与部署
Nagios概述: Nagios是一款开源免费(也有收费版的Nagios XI)的监控工具,可以用以监控Windows.Linux.Unix.Router.Switch,可以监控指定主机的物理基础资源 ...
- ASP.NET Web API 2 框架揭秘
这不是一本传统意义上的入门书籍 任何 —本书都具有对应的受众群体,所以我不得不将这句话放在最前面,并且希望所有 打算购买此书的读者能够看到.如果你之前对As氵NET W山API(或者AsPNET MⅤ ...
- SSM-WebMVC(三)
SSM-WebMVC(三) 一.Annotated Controllers 应用程序控制器 handlerMethod(处理方法) ㈠方法入参 (springmvc针对于在controller ...