前言

先思考一下下面几个问题,本篇文章也将围绕着这几个问题来介绍。

  1. 为什么 Node.js 环境下的全局对象 global 身上并没有 __dirname__filename,而我们在模块中却可以访问到它们呢?它们是从哪来的?
  2. 模块中的 this module.exports exports 三者之间有什么关系呢?

目录结构:

  1. .
  2. ├── index.js
  3. └── module1.js

模块相关代码:

  1. // ./module1.js
  2. console.log("当前模块目录路径 => ", __dirname)
  3. console.log("当前模块文件路径 => ", __filename)
  4. console.log('global.__dirname => ', global.__dirname)
  5. console.log('global.__filename => ', global.__filename)
  6. console.log('this === exports => ', this === exports)
  7. console.log('this === exports === module.exports => ', this === exports === module.exports)
  8. this.m = 5;
  9. exports.c = 3;
  10. module.exports = {
  11. a: 1,
  12. b: 2
  13. }
  14. console.log('this === exports => ', this === exports)
  15. console.log('this === exports === module.exports => ', this === exports === module.exports)
  1. // index.js
  2. const m1 = require('./module1.js')
  3. console.log('./module1.js 导入的内容 => ', m1)
  4. require('./module1.js')
  5. require('./module1.js')
  6. require('./module1.js')

执行结果:

  1. huyouda@HuyoudeMacBook-Pro 22-09-12 % node "/Users/huyouda/Desktop/22-09-12/index.js"
  2. 当前模块目录路径 => /Users/huyouda/Desktop/22-09-12
  3. 当前模块文件路径 => /Users/huyouda/Desktop/22-09-12/module1.js
  4. global.__dirname => undefined
  5. global.__filename => undefined
  6. this === exports => true
  7. this === exports === module.exports => false
  8. this === exports => true
  9. this === exports === module.exports => false
  10. ./module1.js 导入的内容 => { a: 1, b: 2 }

接下来要做的,就是介绍清楚,为什么输出结果是这样 👆🏻 的。

原理浅析:

简单来说,当我们使用 require 函数去导入一个模块的时候,模块中的内容会被读取,然后丢到一个函数中去执行,调用函数时,会传入一系列参数(__dirname __filename 就是通过参数传递给模块的)。当然,在导入模块之前,会先看缓存,如果有缓存,那么就直接从缓存中取。

下面通过一段 require 的伪代码来了解一下 require('./module1.js') 执行之后,都发生了什么。

  1. // 伪代码
  2. function require(modulePath) {
  3. // step1 - 将 xxx 转为绝对路径(一个绝对路径,对应的模块一定是唯一的,可以使用绝对路径作为模块的唯一标识)
  4. modulePath = require.resolve("xxx")
  5. // step2 - 查看 require 对象的缓存 require.cache 中是否含有该模块
  6. if (require.cache[modulePath]) return require.cache[modulePath] // 有缓存,则直接导出缓存,停止查找
  7. // step3 - 没缓存,则读取文件内容,并将文件内容封装到一个函数中
  8. function __temp(module, exports, require, __dirname, __filename) {
  9. // 文件内容直接丢到该函数中,作为该函数的函数体
  10. console.log("当前模块目录路径 => ", __dirname)
  11. console.log("当前模块文件路径 => ", __filename)
  12. console.log('global.__dirname => ', global.__dirname)
  13. console.log('global.__filename => ', global.__filename)
  14. console.log('this === exports => ', this === exports)
  15. console.log('this === exports === module.exports => ', this === exports === module.exports)
  16. this.m = 5;
  17. exports.c = 3;
  18. module.exports = {
  19. a: 1,
  20. b: 2
  21. }
  22. console.log('this === exports => ', this === exports)
  23. console.log('this === exports === module.exports => ', this === exports === module.exports)
  24. }
  25. // step4 - 给 module 对象初始化一个 exports 成员,最终要导出该成员
  26. module.exports = {};
  27. const exports = module.exports;
  28. // step5 - 调用 __temp
  29. __temp.call(module.exports, module, exports, require, module.path, module.filename) // 这一步很关键,它充分证明了一点:模块中的 module、this、exports、require、__dirname、__filename 都是啥。
  30. // step6 - 将 module.exports 导出
  31. return module.exports;
  32. }

查找模块

modulePath = require.resolve("xxx")

这个查找模块的水比较深,伪代码中这么写,只是简单表示表示罢了,这一步需要知道一点即可,就是只要我们传入的路径能够找到模块,那么第一步就是获取到「模块的绝对路径」。

模块的绝对路径将作为模块的 id(准确地说,应该是 key)

可以在 index.js 模块中,将模块缓存 require.cache 输出看看 console.log('require.cache => ', require.cache)

其中 '/Users/huyouda/Desktop/22-09-12/module1.js' 就是经过解析后的 module1.js 模块的 key,通过这个 key 就可以找到 module1.js 模块。

  1. require.cache => [Object: null prototype] {
  2. '/Users/huyouda/Desktop/22-09-12/index.js': Module {
  3. id: '.',
  4. path: '/Users/huyouda/Desktop/22-09-12',
  5. exports: {},
  6. filename: '/Users/huyouda/Desktop/22-09-12/index.js',
  7. loaded: false,
  8. children: [ [Module] ],
  9. paths: [
  10. '/Users/huyouda/Desktop/22-09-12/node_modules',
  11. '/Users/huyouda/Desktop/node_modules',
  12. '/Users/huyouda/node_modules',
  13. '/Users/node_modules',
  14. '/node_modules'
  15. ]
  16. },
  17. '/Users/huyouda/Desktop/22-09-12/module1.js': Module {
  18. id: '/Users/huyouda/Desktop/22-09-12/module1.js',
  19. path: '/Users/huyouda/Desktop/22-09-12',
  20. exports: { a: 1, b: 2 },
  21. filename: '/Users/huyouda/Desktop/22-09-12/module1.js',
  22. loaded: true,
  23. children: [],
  24. paths: [
  25. '/Users/huyouda/Desktop/22-09-12/node_modules',
  26. '/Users/huyouda/Desktop/node_modules',
  27. '/Users/huyouda/node_modules',
  28. '/Users/node_modules',
  29. '/node_modules'
  30. ]
  31. }
  32. }

检查缓存

if (require.cache[modulePath]) return require.cache[modulePath]

在理解了 step1 的基础上,再来看 step2,就非常简单了,其实就是看一下 require.cache 中,有没有 modulePath

  • 如果有缓存,直接将缓存给返回,后续逻辑都不用看了
  • 如果没有缓存,则继续后续逻辑

读取文件内容并丢到一个函数中

  1. function __temp(module, exports, require, __dirname, __filename) {
  2. // ... => module1.js 的内容
  3. }

注意一下该函数的参数:

  • module
  • exports
  • require
  • __dirname
  • __filename

模块中之所以能够访问到这些成员,其实都是通过参数传递过来的。

初始化 exports

  • module.exports = {};module 对象初始化一个 exports 成员,最终要导出该成员。
  • const exports = module.exports; 初始化一个 exports 变量,下一步要用

调用 __temp

__temp.call(module.exports, module, exports, require, module.path, module.filename)

这一步很关键,它充分说明了一点:模块中的 modulethisexportsrequire__dirname__filename 都是哪来的。

  • __dirname 实际上就是 module.path
  • __filename 实际上就是 module.filename

回答问题1:为什么 Node.js 环境下的全局对象 global 身上并没有 __dirname__filename,而我们在模块中却可以访问到它们呢?它们是从哪来的?

虽然 global 上,没有 __dirname__filename,但是我们在调用函数 __temp(函数体为模块内容)的时候,给函数传了对应的参数,模块中读取到的,是我们传递过去的 module.pathmodule.filename

this 指向问题:

__temp.call(module.exports, ...)

thisexports 一样,一开始都指向 module.exports

将 module.exports 导出

return module.exports;

回答问题2:模块中的 this module.exports exports 三者之间有什么关系呢?

thisexports 一样,一开始都指向 module.exports

但是模块最终导出的始终是 module.exports,如果我们在模块中,将其重新赋值 module.exports = xxx,那么 thisexports 就形同虚设了。

执行结果分析

执行结果:

  1. huyouda@HuyoudeMacBook-Pro 22-09-12 % node "/Users/huyouda/Desktop/22-09-12/index.js"
  2. 当前模块目录路径 => /Users/huyouda/Desktop/22-09-12
  3. 当前模块文件路径 => /Users/huyouda/Desktop/22-09-12/module1.js
  4. global.__dirname => undefined
  5. global.__filename => undefined
  6. this === exports => true
  7. this === exports === module.exports => false
  8. this === exports => true
  9. this === exports === module.exports => false
  10. ./module1.js 导入的内容 => { a: 1, b: 2 }

问题1、问题2,都已经解决了,现在还剩下一个问题:require('./module1.js') 在 index.js 中执行了 4 次,为什么模块 ./module1.js 只执行了一次呢?

其实这个问题在 「1.2」 中就已经说明了,因为第一次导入模块之后,模块被缓存了,后边的 3 次导入,发现是已被缓存的模块,一开始就直接 return 了,__temp 函数压根就没执行。

PS:伪代码中并没有体现「将模块缓存起来」的逻辑,只要知道模块被导入之后,是会被缓存起来的即可。