commonjs 深度理解

77 阅读3分钟

从Script开始理解模块化中已经提及了commonjs的使用和特点了,但更深层的内容还没有讲到,比如循环引用,require是哪来的,exportsmodule.exports的关系等等

定义

CommonJS (CJS)是一种模块化规范,最初由 Mozilla、Node.js 和其他组织共同制定,旨在为 JavaScript 提供一种在服务器端(如 Node.js)进行模块化编程的方式。

但是commonjs并不只能作用在服务器端,例如在webpackBrowserify打包工具对 commonJS 的支持和转换;

require('xxxx')的实现

我们引用一个模块,会通过require 来实现,区别在于模块标识,模块标识分为以下几类:

模块标识

  1. 核心模块
const path = require('path')
  1. 自定义js文件模块
  • 以 ./ 或者 ../../././等相对路径
const moduleA = require('./A.js')
  1. 第三方模块
const axios = require('axios')

但是require具体是怎么工作的呢?这需要理解加载流程

加载流程(重点)

在node环境中加载模块会经历三个阶段:

  1. 路径分析
  • 路径分析会针对模块标识判断是核心模块还是自定模块
  1. 文件定位
  • 针对核心模块的话:核心模块在node源代码编译的过程中已经编译成二进制文件,部分核心模块加载进缓存当中
  • 针对相对路径的文件模块的话:会根据相对路径一层一层往上找,别名依次.js.json.node等
  • 针对第三方模块时:定位最慢,会从当前路径一层一层往上找node_modules
console.log(module.paths);
/*
终端执行: node module_path.js
[
  'E:\学习\test-demo\src\node_modules',
  'E:\学习\test-demo\node_modules',     
  'E:\学习\node_modules',
  'E:\node_modules'
]
*/
  1. 编译
  • 核心模块:无需执行,node源代码编译中已经编译成二进制文件
  • 其他:通过定位找到模块编译执行,node环境下会把js文件编译成如下代码没有执行,所以我们能在js文件中使用exports,require等关键字

(function (exports, require, module, __filename, __dirname) { 
 var math = require('math'); 
 exports.area = function (radius) { 
 return Math.PI * radius * radius; 
 }; 
})

require的实现

上面只是阐述了编译,当我们通过require来执行模块内的代码时,require是这么做的

function require(id) {
 var cachedModule = Module._cache[id];
  if(cachedModule){
    return cachedModule.exports;
  }
  
  const module = { exports: {} }

  // 这里先将引用加入缓存 后面循环引用会说到
  Module._cache[id] = module

  //runInThisContext 类似于 eval 执行代码
  runInThisContext(wrapper('module.exports = "123"'))(module.exports, require, module, 'filename', 'dirname')


  return module.exports
}

主要步骤四点

  1. 从Module._cache 中取缓存,有缓存则返回缓存,不执行代码
  2. 缓存没有命中,则先加入缓存
  3. 加入缓存之后执行runInThisContext函数,传入exports,require,module,filename,dirname五个函数
  4. 返回module.exports属性值

这里的Module可以看成是一个全局Module,专门用来缓存模块文件

循环引用

通过require的实现我们知道,有缓存先缓存,没有缓存直接缓存再执行,这里就隔绝了循环引用

module.exports 与 exports

先说结论

module.exports === exports // true两者是同一个东西

exports = {aa:1} 无效

var a = 1;
exports = {a}
console.log("a:",exports) // a: { a: 1 }
console.log("index:",require('./module/a')) // {}

这里是在nodejs环境中的形参,exports={a} 相当于重新创建了一个变量exports,而不是原有的那个,因为原有的exports是被当作参数穿进去的

runInThisContext(wrapper('module.exports = "123"'))(module.exports, require, module, 'filename', 'dirname')

module.exports和exports的区别

  1. exports是module上的一个属性
  2. module.exports可以赋值一个函数,直接全量导出,exports不行
require('./module/a')() // 1

// a.js
module.exports = function say(){
  console.log(1)
}
  1. 同时存在,module.exports 会覆盖exports
console.log(require('./module/a')) // 1

// a.js
module.exports = 1
exports.obj = {
  name:'lian'
}
  1. 两个都不能异步导出

QA

  1. commonjs 为什么无法tree shaking
  • cjs是在运行时加载的,无法在编译时通过摇树出去不需要的代码
  1. exports可以异步导出吗
  • 不行,只能通过promise包裹导出