在从Script开始理解模块化中已经提及了commonjs
的使用和特点了,但更深层的内容还没有讲到,比如循环引用,require
是哪来的,exports
和module.exports
的关系等等
定义
CommonJS (CJS)
是一种模块化规范,最初由 Mozilla、Node.js 和其他组织共同制定,旨在为 JavaScript 提供一种在服务器端(如 Node.js)进行模块化编程的方式。
但是commonjs并不只能作用在服务器端,例如在webpack
和Browserify
打包工具对 commonJS 的支持和转换;
require('xxxx')的实现
我们引用一个模块,会通过require 来实现,区别在于模块标识,模块标识分为以下几类:
模块标识
- 核心模块
const path = require('path')
- 自定义js文件模块
- 以 ./ 或者 ../../././等相对路径
const moduleA = require('./A.js')
- 第三方模块
const axios = require('axios')
但是require具体是怎么工作的呢?这需要理解加载流程
加载流程(重点)
在node环境中加载模块会经历三个阶段:
- 路径分析
- 路径分析会针对模块标识判断是核心模块还是自定模块
- 文件定位
- 针对核心模块的话:核心模块在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'
]
*/
- 编译
- 核心模块:无需执行,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
}
主要步骤四点
- 从Module._cache 中取缓存,有缓存则返回缓存,不执行代码
- 缓存没有命中,则先加入缓存
- 加入缓存之后执行runInThisContext函数,传入exports,require,module,filename,dirname五个函数
- 返回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的区别
- exports是module上的一个属性
- module.exports可以赋值一个函数,直接全量导出,exports不行
require('./module/a')() // 1
// a.js
module.exports = function say(){
console.log(1)
}
- 同时存在,module.exports 会覆盖exports
console.log(require('./module/a')) // 1
// a.js
module.exports = 1
exports.obj = {
name:'lian'
}
- 两个都不能异步导出
QA
- commonjs 为什么无法tree shaking
- cjs是在运行时加载的,无法在编译时通过摇树出去不需要的代码
- exports可以异步导出吗
- 不行,只能通过promise包裹导出