前言
先思考一下下面几个问题,本篇文章也将围绕着这几个问题来介绍。
- 为什么 Node.js 环境下的全局对象 global 身上并没有
__dirname
和__filename
,而我们在模块中却可以访问到它们呢?它们是从哪来的? - 模块中的
this
module.exports
exports
三者之间有什么关系呢?
目录结构:
.
├── index.js
└── module1.js
模块相关代码:
// ./module1.js
console.log("当前模块目录路径 => ", __dirname)
console.log("当前模块文件路径 => ", __filename)
console.log('global.__dirname => ', global.__dirname)
console.log('global.__filename => ', global.__filename)
console.log('this === exports => ', this === exports)
console.log('this === exports === module.exports => ', this === exports === module.exports)
this.m = 5;
exports.c = 3;
module.exports = {
a: 1,
b: 2
}
console.log('this === exports => ', this === exports)
console.log('this === exports === module.exports => ', this === exports === module.exports)
// index.js
const m1 = require('./module1.js')
console.log('./module1.js 导入的内容 => ', m1)
require('./module1.js')
require('./module1.js')
require('./module1.js')
执行结果:
huyouda@HuyoudeMacBook-Pro 22-09-12 % node "/Users/huyouda/Desktop/22-09-12/index.js"
当前模块目录路径 => /Users/huyouda/Desktop/22-09-12
当前模块文件路径 => /Users/huyouda/Desktop/22-09-12/module1.js
global.__dirname => undefined
global.__filename => undefined
this === exports => true
this === exports === module.exports => false
this === exports => true
this === exports === module.exports => false
./module1.js 导入的内容 => { a: 1, b: 2 }
接下来要做的,就是介绍清楚,为什么输出结果是这样 👆🏻 的。
原理浅析:
简单来说,当我们使用 require 函数去导入一个模块的时候,模块中的内容会被读取,然后丢到一个函数中去执行,调用函数时,会传入一系列参数(__dirname
__filename
就是通过参数传递给模块的)。当然,在导入模块之前,会先看缓存,如果有缓存,那么就直接从缓存中取。
下面通过一段 require 的伪代码来了解一下 require('./module1.js')
执行之后,都发生了什么。
// 伪代码
function require(modulePath) {
// step1 - 将 xxx 转为绝对路径(一个绝对路径,对应的模块一定是唯一的,可以使用绝对路径作为模块的唯一标识)
modulePath = require.resolve("xxx")
// step2 - 查看 require 对象的缓存 require.cache 中是否含有该模块
if (require.cache[modulePath]) return require.cache[modulePath] // 有缓存,则直接导出缓存,停止查找
// step3 - 没缓存,则读取文件内容,并将文件内容封装到一个函数中
function __temp(module, exports, require, __dirname, __filename) {
// 文件内容直接丢到该函数中,作为该函数的函数体
console.log("当前模块目录路径 => ", __dirname)
console.log("当前模块文件路径 => ", __filename)
console.log('global.__dirname => ', global.__dirname)
console.log('global.__filename => ', global.__filename)
console.log('this === exports => ', this === exports)
console.log('this === exports === module.exports => ', this === exports === module.exports)
this.m = 5;
exports.c = 3;
module.exports = {
a: 1,
b: 2
}
console.log('this === exports => ', this === exports)
console.log('this === exports === module.exports => ', this === exports === module.exports)
}
// step4 - 给 module 对象初始化一个 exports 成员,最终要导出该成员
module.exports = {};
const exports = module.exports;
// step5 - 调用 __temp
__temp.call(module.exports, module, exports, require, module.path, module.filename) // 这一步很关键,它充分证明了一点:模块中的 module、this、exports、require、__dirname、__filename 都是啥。
// step6 - 将 module.exports 导出
return module.exports;
}
查找模块
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 模块。
require.cache => [Object: null prototype] {
'/Users/huyouda/Desktop/22-09-12/index.js': Module {
id: '.',
path: '/Users/huyouda/Desktop/22-09-12',
exports: {},
filename: '/Users/huyouda/Desktop/22-09-12/index.js',
loaded: false,
children: [ [Module] ],
paths: [
'/Users/huyouda/Desktop/22-09-12/node_modules',
'/Users/huyouda/Desktop/node_modules',
'/Users/huyouda/node_modules',
'/Users/node_modules',
'/node_modules'
]
},
'/Users/huyouda/Desktop/22-09-12/module1.js': Module {
id: '/Users/huyouda/Desktop/22-09-12/module1.js',
path: '/Users/huyouda/Desktop/22-09-12',
exports: { a: 1, b: 2 },
filename: '/Users/huyouda/Desktop/22-09-12/module1.js',
loaded: true,
children: [],
paths: [
'/Users/huyouda/Desktop/22-09-12/node_modules',
'/Users/huyouda/Desktop/node_modules',
'/Users/huyouda/node_modules',
'/Users/node_modules',
'/node_modules'
]
}
}
检查缓存
if (require.cache[modulePath]) return require.cache[modulePath]
在理解了 step1 的基础上,再来看 step2,就非常简单了,其实就是看一下 require.cache
中,有没有 modulePath
- 如果有缓存,直接将缓存给返回,后续逻辑都不用看了
- 如果没有缓存,则继续后续逻辑
读取文件内容并丢到一个函数中
function __temp(module, exports, require, __dirname, __filename) {
// ... => module1.js 的内容
}
注意一下该函数的参数:
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)
这一步很关键,它充分说明了一点:模块中的 module
、this
、exports
、require
、__dirname
、__filename
都是哪来的。
__dirname
实际上就是module.path
__filename
实际上就是module.filename
回答问题1:为什么 Node.js 环境下的全局对象 global 身上并没有
__dirname
和__filename
,而我们在模块中却可以访问到它们呢?它们是从哪来的?虽然
global
上,没有__dirname
、__filename
,但是我们在调用函数__temp
(函数体为模块内容)的时候,给函数传了对应的参数,模块中读取到的,是我们传递过去的module.path
和module.filename
this 指向问题:
__temp.call(module.exports, ...)
this
和 exports
一样,一开始都指向 module.exports
将 module.exports 导出
return module.exports;
回答问题2:模块中的
this
module.exports
exports
三者之间有什么关系呢?
this
和exports
一样,一开始都指向module.exports
但是模块最终导出的始终是
module.exports
,如果我们在模块中,将其重新赋值module.exports = xxx
,那么this
和exports
就形同虚设了。
执行结果分析
执行结果:
huyouda@HuyoudeMacBook-Pro 22-09-12 % node "/Users/huyouda/Desktop/22-09-12/index.js"
当前模块目录路径 => /Users/huyouda/Desktop/22-09-12
当前模块文件路径 => /Users/huyouda/Desktop/22-09-12/module1.js
global.__dirname => undefined
global.__filename => undefined
this === exports => true
this === exports === module.exports => false
this === exports => true
this === exports === module.exports => false
./module1.js 导入的内容 => { a: 1, b: 2 }
问题1、问题2,都已经解决了,现在还剩下一个问题:require('./module1.js')
在 index.js 中执行了 4 次,为什么模块 ./module1.js
只执行了一次呢?
其实这个问题在 「1.2」 中就已经说明了,因为第一次导入模块之后,模块被缓存了,后边的 3 次导入,发现是已被缓存的模块,一开始就直接 return
了,__temp
函数压根就没执行。
PS:伪代码中并没有体现「将模块缓存起来」的逻辑,只要知道模块被导入之后,是会被缓存起来的即可。