commonJs与ESModule的区别
开篇
学习之前引出几个问题:
- Commonjs 和 Es Module 有什么区别 ?
- Commonjs 如何解决的循环引用问题 ?
- Es Module 如何解决循环引用问题 ?
- exports 和 module.exports 有何不同?
- require 模块查找机制 ?
- import() 的动态引入?
- Es Module 如何改变模块下的私有变量 ?
模块化
开发很容易存在全局污染和依赖管理混乱问题,所以就需要模块化来解决这两个问题,今天介绍的是前端模块化的两个重要方案,commonjs 和 ESModule
Commonjs
commonjs的提出弥补了,前端模块化的空缺,nodejs借鉴了commonjs,实现了模块化管理。
目前commonjs广泛应用于以下几个场景:
- Node(commonjs在服务端的一个具体代表性实现)
- Browserify( commonjs 在浏览器中的一种实现)
- wepack 打包工具对 CommonJS 的支持和转换
特点
- 在commonjs中每个js文件就是一个单独的模块,称为module。
- 模块中包含commonjs的核心变量:exports、module.exports、require
- exports 和 module.export可以用于导出模块中的内容
- require 函数可以帮助我们导入其他模块(自定义模块、系统模块、第三方库模块)中的内容
实现原理
每个模块文件上存在 module,exports,require三个变量,然而这三个变量是没有被定义的,但是我们可以在 Commonjs 规范下每一个 js 模块上直接使用它们
- module 记录当前模块信息。
- require 引入模块的方法。
- exports 当前模块导出的属性
在编译的过程中,实际 Commonjs 对 js 的代码块进行了首尾包装
1 |
|
在 Commonjs 规范下模块中,会形成一个包装函数,我们写的代码将作为包装函数的执行上下文,使用的 require ,exports ,module 本质上是通过形参的方式传递到包装函数中的。
1 |
|
包装函数执行
1 |
|
1 |
|
在模块加载的时候,会通过 runInThisContext (可以理解成 eval ) 执行 modulefunction ,传入require ,exports ,module 等参数。最终我们写的 nodejs 文件就这么执行了。
require 文件加载流程
1 |
|
如上所示:require可以加载核心模块、文件模块、第三方自定义模块。
当 require 方法执行的时候,接收的唯一参数作为一个标识符 ,Commonjs 下对不同的标识符,处理流程不同,但是目的相同,都是找到对应的模块。
require 加载标识符原则
nodejs中对标识符的处理原则
- 对fs、http、path等标识符,会被作为核心模块
- ./和../作为相对路径的文件模块,/作为绝对路径的文件模块
- 非路径形式也非核心模块的模块,将作为自定义模块
核心模块的处理:
核心模块的优先级仅次于缓存加载,在 Node 源码编译中,已被编译成二进制代码,所以加载核心模块,加载过程中速度最快。
路径形式的文件模块处理:
已 ./ ,../ 和 / 开始的标识符,会被当作文件模块处理。require() 方法会将路径转换成真实路径,并以真实路径作为索引,将编译后的结果缓存起来,第二次加载的时候会更快。
自定义模块处理:
自定义模块,一般指的是非核心的模块,它可能是一个文件或者一个包,它的查找会遵循以下原则:
- 在当前目录下的 node_modules 目录查找。
- 如果没有,在父级目录的 node_modules 查找,如果没有在父级目录的父级目录的 node_modules 中查找
- 沿着路径向上递归,直到根目录下的 node_modules 目录。
- 在查找过程中,会找 package.json 下 main 属性指向的文件,如果没有 package.json ,在 node 环境下会以此查找 index.js ,index.json ,index.node。
require 模块引入与处理
CommonJS 模块同步加载并执行模块文件,CommonJS 模块在执行阶段分析模块依赖,采用深度优先遍历(depth-first traversal),执行顺序是父 -> 子 -> 父;
require 加载原理
过程:
require 会接收一个参数——文件标识符,然后分析定位文件,分析过程我们上述已经讲到了,加下来会从 Module 上查找有没有缓存,如果有缓存,那么直接返回缓存的内容。
如果没有缓存,会创建一个 module 对象,缓存到 Module 上,然后执行文件,加载完文件,将 loaded 属性设置为 true ,然后返回 module.exports 对象。借此完成模块加载流程。
模块导出就是 return 这个变量的其实跟 a = b 赋值一样, 基本类型导出的是值, 引用类型导出的是引用地址。
exports 和 module.exports 持有相同引用,因为最后导出的是 module.exports, 所以对 exports 进行赋值会导致 exports 操作的不再是 module.exports 的引用。
require 避免重复加载
加载之后的文件的 module 会被缓存到 Module 上,
如果其他模块再次引入,则会直接读取缓存中的,无需再次执行模块
require 避免循环引用
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!