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
2
3
4
5
6
7
8
9
(function(exports,require,module,__filename,__dirname){
const sayName = require('./hello.js')
module.exports = function say(){
return {
name:sayName(),
author:'YoLin'
}
}
})

在 Commonjs 规范下模块中,会形成一个包装函数,我们写的代码将作为包装函数的执行上下文,使用的 require ,exports ,module 本质上是通过形参的方式传递到包装函数中的。

1
2
3
4
5
function wrapper (script) {
return '(function (exports, require, module, __filename, __dirname) {' +
script +
'\n})'
}

包装函数执行

1
2
3
4
5
6
7
8
9
const modulefunction = wrapper(`
const sayName = require('./hello.js')
module.exports = function say(){
return {
name:sayName(),
author:'YoLin'
}
}
`)
1
runInThisContext(modulefunction)(module.exports, require, module, __filename, __dirname)

在模块加载的时候,会通过 runInThisContext (可以理解成 eval ) 执行 modulefunction ,传入require ,exports ,module 等参数。最终我们写的 nodejs 文件就这么执行了。

require 文件加载流程

1
2
3
1const fs = require('fs') // 核心模块
2const sayName = require('./sayName.js') // 文件模块
3const crypto = require('crypto-js') // 第三方模块

如上所示: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 协议 ,转载请注明出处!