再有人问你CommonJS​,把这篇文章丢给他

62 阅读4分钟

介绍

CommonJS(CJS)最初提出来是在浏览器以外的地方使用,Node.js 是 CommonJS 在服务端的一个实现,Node.js 的每一个文件都是单独的模块。

核心函数

exports / module.exports / require()CommonJS 的核心内容。

  • exportsmodule.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.jsfoo 变量,所以 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 中执行修改,效果也是类似,这里不再赘述了:

image.png

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)
  • 验证结果:

image.png

可以看出,最终导出的是 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 变量中我们可以看出来:

image.png

执行

  • 模块在第一次加载时,会被自上而下执行一次,并会被缓存。

  • 模块在第二次加载时,不会再自上而下执行,而是直接获取缓存的内容。

例如:

foo.js

console.log('foo execute!')

index.js

const foo = require('./foo')
const foo2 = require('./foo')

console.log(foo)
console.log(foo2)

将得到输出,可以发现只执行了一次:

image.png

关于循环引用

  • 请看下面的结构图,b -> c -> d -> b 出现了循环引用。

  • Node.js 在遍历模块时采用了深度优先遍历的方法。

  • 所以实际的搜索顺序为 main -> a -> c -> d -> b,每一次搜索都会缓存。

image.png

  • 如果是一个单循环呢:

image.png

如果 b 依赖 a,而 a 又没执行完毕,那么 b 中 requireA 的结果将是 undefined。

特点

从上面的各个案例我们发现,CommonJS 加载模式是同步的,也就是说,只有等到 require 的目标模块加载完毕,我们才可以运行相应的模块,这个方案在浏览器上不太好 -- 我们需要将对应的模块下载下来、再开始运行,体验很差。

另外,CommonJS 的动态性很强,我们可以在代码中不受约束地 require -- 静态分析难以发挥作用,因此一般情况下 CommonJS 无法实现 TreeShaking。

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 19 天,点击查看活动详情