介绍
CommonJS(CJS)最初提出来是在浏览器以外的地方使用,Node.js 是 CommonJS 在服务端的一个实现,Node.js 的每一个文件都是单独的模块。
核心函数
exports
/ module.exports
/ require()
是 CommonJS
的核心内容。
-
exports
和module.exports
负责导出模块中的内容。 -
require
函数可以帮助我们导入其他模块中的内容。
exports
exports 是一个对象,我们通过为这个对象赋值添加属性,添加的属性会被导出,例如下面的代码:
foo.js的代码如下:
const name = 'zayyo'
const age = 8
const sayHello = () => {
console.log('hello world!')
}
exports.name = name
exports.age = age
exports.sayHello = sayHello
main.js 的代码如下:
const foo = require('./foo')
console.log(foo.name)
console.log(foo.age)
foo.sayHello()
注意 const foo = require('./foo')
这个导入语法,它的本质是从上到下执行 foo.js
并将 该模块中的 exports
对象赋值给 main.js
的 foo 变量,所以 foo 变量就是对 export 的引用。
下面我们进行验证:
- 修改
main.js
内容,添加一个 setTimeout,延时为 0 将其添加到任务队列中:
const foo = require('./foo')
console.log(foo.name)
console.log(foo.age)
foo.sayHello()
setTimeout(() => {
console.log(foo.name)
}, 0)
- 以类似的方法修改 foo.js 内容,同样使用 setTimeout 将其添加到任务队列中,其中的代码一定是在 main.js 的 setTimeout 之前执行、第五行之后执行(如果不理解,可以了解一下 node 的事件循环机制):
const name = 'zayyo'
const age = 8
const sayHello = () => {
console.log('hello world!')
}
exports.name = name
exports.age = age
exports.sayHello = sayHello
setTimeout(() => {
exports.name = 'zayyo1'
}, 0)
- 查看输出,验证无误,当然我们如果在 main.js 中执行修改,效果也是类似,这里不再赘述了:
module.exports
我平时使用 Node.js 时,一般都是通过 module.exports 实现导出的,那么它和 exports 有什么区别?
-
事实上,CommonJS 规范中并没有提到 module.exports。
-
Node.js 使用 Module 类实现模块化。
-
module.exports 实际上就是对 exports 的引用,我们导出最终其实导出的是 module.exports,它是个对象。
下面我们给出验证:
- foo.js
const name = 'zayyo'
const age = 8
const sayHello = () => {
console.log('hello world!')
}
exports.name = name
exports.age = age
exports.sayHello = sayHello
module.exports = {
name: 'zayyo1',
age: 9
}
- main.js
const foo = require('./foo')
console.log(foo.name)
console.log(foo.age)
- 验证结果:
可以看出,最终导出的是 module.exports,原理是在 foo.js 中我们为 module.exports 赋值了一个新的对象,此时 module.exports 就不再指向 exports 了!
模块的加载顺序与流程
顺序
Node.js 的模块加载规则如下:
-
如果当前模块是核心模块,例如
require('path')
,直接返回对应的核心模块。 -
如果是一个路径,例如
require('./foo')
或者require('./foo.js')
-
首先Node.js会尝试将 foo 当成一个确切的文件名来查找。
-
其次如果按确切的文件名找不到模块,则 Node.js 会尝试带上 .js、 .json 或 .node 扩展名再加载,查找完成直接返回。
-
如果上述步骤还是找不到,我们将把 foo 当成一个目录,依次查找其下的 index.js、index.json、index.node 文件
-
如果它不是一个路径(例如一些第三方模块),那么我们会从当前目录的 node_modules 开始搜索、直到根目录,从 module.paths 变量中我们可以看出来:
执行
-
模块在第一次加载时,会被自上而下执行一次,并会被缓存。
-
模块在第二次加载时,不会再自上而下执行,而是直接获取缓存的内容。
例如:
foo.js
console.log('foo execute!')
index.js
const foo = require('./foo')
const foo2 = require('./foo')
console.log(foo)
console.log(foo2)
将得到输出,可以发现只执行了一次:
关于循环引用
-
请看下面的结构图,b -> c -> d -> b 出现了循环引用。
-
Node.js 在遍历模块时采用了深度优先遍历的方法。
-
所以实际的搜索顺序为 main -> a -> c -> d -> b,每一次搜索都会缓存。
- 如果是一个单循环呢:
如果 b 依赖 a,而 a 又没执行完毕,那么 b 中 requireA 的结果将是 undefined。
特点
从上面的各个案例我们发现,CommonJS 加载模式是同步的,也就是说,只有等到 require 的目标模块加载完毕,我们才可以运行相应的模块,这个方案在浏览器上不太好 -- 我们需要将对应的模块下载下来、再开始运行,体验很差。
另外,CommonJS 的动态性很强,我们可以在代码中不受约束地 require -- 静态分析难以发挥作用,因此一般情况下 CommonJS 无法实现 TreeShaking。
开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 19 天,点击查看活动详情