commonjs和ESM 实现原理

815 阅读4分钟

前言

之前对于模块化开发了解的不多,但是在项目中又经常用到,每次使用module.exports和exports的时候总是搞不明白,但是经常看同事写这块写的可顺手了,所以就下定决心把这块原理搞明白

前端为什么需要模块化开发

模块化开发的好处有很多例如解决命名冲突、提高可复用性、提高代码可维护性等等,但是这里我想说其中的两点解决全局污染和依赖管理,下面上代码 首先在a.js中

function show(){
 name = '我是a.js中的name'
}

那么在index.html

<script src="./a.js"></script>
<body>
  <script>
        name = "我是index.js中定义的name"
        show();
        console.log(name);
  </script>
</body>

那么在控制台打印出的name是

我是a.js中定义的name 这只是其中的一个小例子,那么如果好多人开发不同的js,在引用的时候肯定会出现很多问题

那么还有我一个问题就是依赖管理,就是在script标签中引入的js是按排列的顺序执行的,那么排列在下面的js去调用已经执行完的js中的方法是无法调用的 那么通过以上例子我们就说一下前端两个模块化的方案 Commonjs和Es Module

Commonjs

特点

  • 在commonsjs中每一个js文件都是一个单独的模块我们可以称之为module

  • 该模块中,包含Commonjs规范的核心变量:exports、module.exports、require,在nodejs中还存在_filename和_dirname变量

  • exports和module.exports可以负责对模块中的内容进行导出

  • require函数可以帮助我们导入其他模块(自定义模块、系统模块、第三方模块库)中的内容

前面我们提到每个模块中都包含三个变量,三个变量的意思分别是

  • module记录当前模块信息
  • require引入模块的方法
  • exports当前模块导出的属性

在编译过程中实际上commonjs对js中的代码进行了首位包装 例如在一个main.js中引入了index.js中的方法hello

function(exports, require,module,_filename,_dirname){
  const hello = require('./index.js')
  module.exports = function say(){
   return {
    talk: hello(),
    name: '前端开发'
   }
  }
}
)

我们称外面的函数为包装函数,那么也就是说在编译的时候exports,require,module其实是通过形参的方式传递到包装函数中的 那么包装函数又是什么样呢

function wrapper (script) {
  return '(function (exports, require,module,_filename,_dirname){' + 
  script + 
  '\n})'
}
const modulefuntion = wrapper('
 const hello = require('./index.js')
  module.exports = function say(){
   return {
    talk: hello(),
    name: '前端开发'
   }
  }
')

那么script就是我们在main.js模块中写的内容

runInThisContext(modulefunction)(module.exports, require,module,_filename,_dirname)

在模块加载的时候,会通过runInThisContext执行modulefunction,传入export,require, module等参数,这就是我们所说的加载流程

require的加载原理

我们先说两个概念module和Module module: 在node中每一个js文件都是一个module,module上保存了exports等信息之外,还有一个loaded表示该模块是否被已经被加载

  • 为false表示还没有加载
  • 为true 表示已经加载 Module: 以nodejs为例,整个系统运行之后,会用Module缓存每一个模块加载的信息 我们看一下require源码
// id 为路径标识符
function require(id) {
  // 查找Module 上有没有已经加载的js对象
  const cachedModule = Module._cache[id]
  // 如果已经加载了那么直接取走缓存的exports对象
  if(cachedModule) {
   return cachedModule.exports
  }
  // 创建当前模块 module
  
  const module = { exports: {}, loaded: false, ...}
  
  //将module缓存到module 的缓存属性中,路径标识符作为id
  Module._cache[id] = module
  // 加载文件
  runInthisContext(wrapper('module.exports = "123"'))(module.exports, require,module,_filename,_dirname)
  
  // 加载完成
  module.loaded = true
  //返回值
  return module.exports
  
}

那么通过上述require大概的源码我们可以总结出几点

  • require 会接受一个参数 -文件标识符,然后分析定位,接下来会从Module上查找有没有缓存,如果有缓存,那么直接返回缓存的内容
  • 如果没有缓存,会创建一个module对象,缓存到Module上,然后执行文件,加载完文件,将loaded属性设置为true,然后返回module.exports对象
  • exports和module.exports持有相同的引用,因为最后导出的是module.exports,所以对exports进行赋值会导致exports操作的不再是module.exports的引用

exports和module.exports

从上述require原理实现中,我们知道了exports和module.exports持有相同的引用,因为最后导出的是module.exports

ES Module

导出export和导入import 所有通过export导出的属性,在import中可以通过解构的方式解构出来 默认导出export default export default anything 导入module的默认导出,anything可以是函数,属性方法,或者对象 那么具体esmodule需要注意的点我这里就不说了,比较基础