파일 모듈
입니다. , 주로 두 가지 유형이 있습니다: 🎜
- 내장 모듈:
fs
,http
등과 같이 Node.js에서 기본적으로 제공하는 함수입니다. 이 모듈은 Node.js가 로드될 때 로드됩니다. .js 프로세스가 시작됩니다.fs
,http
等等,这些模块在Node.js进程起来时就加载了。- 文件模块:我们前面写的几个模块,还有第三方模块,即
node_modules
下面的模块都是文件模块。
加载顺序
加载顺序是指当我们require(X)
时,应该按照什么顺序去哪里找X
,在官方文档上有详细伪代码,总结下来大概是这么个顺序:
- 优先加载内置模块,即使有同名文件,也会优先使用内置模块。
- 不是内置模块,先去缓存找。
- 缓存没有就去找对应路径的文件。
- 不存在对应的文件,就将这个路径作为文件夹加载。
- 对应的文件和文件夹都找不到就去
node_modules
下面找。- 还找不到就报错了。
加载文件夹
前面提到找不到文件就找文件夹,但是不可能将整个文件夹都加载进来,加载文件夹的时候也是有一个加载顺序的:
- 先看看这个文件夹下面有没有
package.json
,如果有就找里面的main
字段,main
字段有值就加载对应的文件。所以如果大家在看一些第三方库源码时找不到入口就看看他package.json
里面的main
字段吧,比如jquery
的main
字段就是这样:"main": "dist/jquery.js"
。- 如果没有
package.json
或者package.json
里面没有main
就找index
文件。- 如果这两步都找不到就报错了。
支持的文件类型
require
主要支持三种文件类型:
- .js:
.js
文件是我们最常用的文件类型,加载的时候会先运行整个JS文件,然后将前面说的module.exports
作为require
的返回值。- .json:
.json
文件是一个普通的文本文件,直接用JSON.parse
将其转化为对象返回就行。- .node:
.node
文件是C++编译后的二进制文件,纯前端一般很少接触这个类型。
手写require
" >모듈 유형 및 로딩 순서🎜🎜이 섹션의 내용은 다소 지루한 개념이지만 우리가 이해해야 할 내용이기도 합니다. 🎜모듈 유형
🎜Node.js 모듈에는 여러 유형이 있습니다. 우리가 이전에 사용한 것은 실제로 파일 모듈
입니다. , 주로 두 가지 유형이 있습니다: 🎜- 내장 모듈:
fs
, http
등과 같이 Node.js에서 기본적으로 제공하는 함수입니다. 이 모듈은 Node.js가 로드될 때 로드됩니다. .js 프로세스가 시작됩니다. fs
,http
等等,这些模块在Node.js进程起来时就加载了。- 文件模块:我们前面写的几个模块,还有第三方模块,即
node_modules
下面的模块都是文件模块。
加载顺序
- 내장 모듈:
fs
,http
등과 같이 Node.js에서 기본적으로 제공하는 함수입니다. 이 모듈은 Node.js가 로드될 때 로드됩니다. .js 프로세스가 시작됩니다. - 文件模块:我们前面写的几个模块,还有第三方模块,即
node_modules
下面的模块都是文件模块。
fs
,http
等等,这些模块在Node.js进程起来时就加载了。加载顺序是指当我们require(X)
时,应该按照什么顺序去哪里找X
,在官方文档上有详细伪代码,总结下来大概是这么个顺序:
- 优先加载内置模块,即使有同名文件,也会优先使用内置模块。
- 不是内置模块,先去缓存找。
- 缓存没有就去找对应路径的文件。
- 不存在对应的文件,就将这个路径作为文件夹加载。
- 对应的文件和文件夹都找不到就去
node_modules
下面找。- 还找不到就报错了。
加载文件夹
前面提到找不到文件就找文件夹,但是不可能将整个文件夹都加载进来,加载文件夹的时候也是有一个加载顺序的:
- 先看看这个文件夹下面有没有
package.json
,如果有就找里面的main
字段,main
字段有值就加载对应的文件。所以如果大家在看一些第三方库源码时找不到入口就看看他package.json
里面的main
字段吧,比如jquery
的main
字段就是这样:"main": "dist/jquery.js"
。- 如果没有
package.json
或者package.json
里面没有main
就找index
文件。- 如果这两步都找不到就报错了。
支持的文件类型
require
主要支持三种文件类型:
- .js:
.js
文件是我们最常用的文件类型,加载的时候会先运行整个JS文件,然后将前面说的module.exports
作为require
的返回值。- .json:
.json
文件是一个普通的文本文件,直接用JSON.parse
将其转化为对象返回就行。- .node:
.node
文件是C++编译后的二进制文件,纯前端一般很少接触这个类型。
手写require
Node.js의 모듈 로딩 메커니즘에 대한 심층 분석
모듈은 Node.js에서 매우 기본적이고 중요한 개념입니다. 모듈을 통해 다양한 네이티브 클래스 라이브러리가 제공되며, 타사 라이브러리도 모듈을 통해 관리되고 참조됩니다. 이 기사는 기본 모듈 원리부터 시작하여 결국 이 원리를 사용하여 간단한 모듈 로딩 메커니즘을 직접 구현합니다. 즉, require
을 직접 구현합니다.
Node는 JavaScript 및 commonjs 모듈을 사용하며 npm/yarn을 패키지 관리자로 사용합니다.
【동영상 튜토리얼 추천: node js tutorial】
간단한 예
기존 규칙, 원리를 설명하기 전에 간단한 예를 들고 이 예부터 시작하여 단계별로 원리를 심화시켜 보겠습니다. Node.js에서 특정 콘텐츠를 내보내려면 module.exports
를 사용해야 합니다. module.exports
를 사용하면 문자열과 문자열을 포함한 거의 모든 유형의 JS 개체를 내보낼 수 있습니다. 함수, 객체, 배열 등 먼저 가장 간단한 hello world
:module.exports
,使用module.exports
几乎可以导出任意类型的JS对象,包括字符串,函数,对象,数组等等。我们先来建一个a.js
导出一个最简单的hello world
:
// a.js module.exports = "hello world";
然后再来一个b.js
导出一个函数:
// b.js function add(a, b) { return a + b; } module.exports = add;
然后在index.js
里面使用他们,即require
他们,require
函数返回的结果就是对应文件module.exports
的值:
// index.js const a = require('./a.js'); const add = require('./b.js'); console.log(a); // "hello world" console.log(add(1, 2)); // b导出的是一个加法函数,可以直接使用,这行结果是3
require会先运行目标文件
当我们require
某个模块时,并不是只拿他的module.exports
,而是会从头开始运行这个文件,module.exports = XXX
其实也只是其中一行代码,我们后面会讲到,这行代码的效果其实就是修改模块里面的exports
属性。比如我们再来一个c.js
:
// c.js let c = 1; c = c + 1; module.exports = c; c = 6;
在c.js
里面我们导出了一个c
,这个c
经过了几步计算,当运行到module.exports = c;
这行时c
的值为2
,所以我们require
的c.js
的值就是2
,后面将c
的值改为了6
并不影响前面的这行代码:
const c = require('./c.js'); console.log(c); // c的值是2
前面c.js
的变量c
是一个基本数据类型,所以后面的c = 6;
不影响前面的module.exports
,那他如果是一个引用类型呢?我们直接来试试吧:
// d.js let d = { num: 1 }; d.num++; module.exports = d; d.num = 6;
然后在index.js
里面require
他:
const d = require('./d.js'); console.log(d); // { num: 6 }
我们发现在module.exports
后面给d.num
赋值仍然生效了,因为d
是一个对象,是一个引用类型,我们可以通过这个引用来修改他的值。其实对于引用类型来说,不仅仅在module.exports
后面可以修改他的值,在模块外面也可以修改,比如index.js
里面就可以直接改:
const d = require('./d.js'); d.num = 7; console.log(d); // { num: 7 }
require
和module.exports
不是黑魔法
我们通过前面的例子可以看出来,require
和module.exports
干的事情并不复杂,我们先假设有一个全局对象{}
,初始情况下是空的,当你require
某个文件时,就将这个文件拿出来执行,如果这个文件里面存在module.exports
,当运行到这行代码时将module.exports
的值加入这个对象,键为对应的文件名,最终这个对象就长这样:
{ "a.js": "hello world", "b.js": function add(){}, "c.js": 2, "d.js": { num: 2 } }
当你再次require
某个文件时,如果这个对象里面有对应的值,就直接返回给你,如果没有就重复前面的步骤,执行目标文件,然后将它的module.exports
加入这个全局对象,并返回给调用者。这个全局对象其实就是我们经常听说的缓存。所以require
和module.exports
并没有什么黑魔法,就只是运行并获取目标文件的值,然后加入缓存,用的时候拿出来用就行。再看看这个对象,因为d.js
是一个引用类型,所以你在任何地方获取了这个引用都可以更改他的值,如果不希望自己模块的值被更改,需要自己写模块时进行处理,比如使用Object.freeze()
,Object.defineProperty()
之类的方法。
模块类型和加载顺序
这一节的内容都是一些概念,比较枯燥,但是也是我们需要了解的。
模块类型
Node.js的模块有好几种类型,前面我们使用的其实都是文件模块
function MyModule(id = '') { this.id = id; // 这个id其实就是我们require的路径 this.path = path.dirname(id); // path是Node.js内置模块,用它来获取传入参数对应的文件夹路径 this.exports = {}; // 导出的东西放这里,初始化为空对象 this.filename = null; // 模块对应的文件名 this.loaded = false; // loaded用来标识当前模块是否已经加载 }
a.js
를 빌드한 다음 함수를 내보내는 b.js
를 만들어 보겠습니다. : 🎜MyModule.prototype.require = function (id) { return Module._load(id); }
index.js
, 즉 🎜그들에서 사용하세요. 🎜함수에 의해 반환된 결과는 해당 파일 module.exports
의 값입니다. 🎜MyModule._cache = Object.create(null); MyModule._load = function (request) { // request是我们传入的路劲参数 const filename = MyModule._resolveFilename(request); // 先检查缓存,如果缓存存在且已经加载,直接返回缓存 const cachedModule = MyModule._cache[filename]; if (cachedModule !== undefined) { return cachedModule.exports; } // 如果缓存不存在,我们就加载这个模块 // 加载前先new一个MyModule实例,然后调用实例方法load来加载 // 加载完成直接返回module.exports const module = new MyModule(filename); // load之前就将这个模块缓存下来,这样如果有循环引用就会拿到这个缓存,但是这个缓存里面的exports可能还没有或者不完整 MyModule._cache[filename] = module; module.load(filename); return module.exports; }
require는 대상 파일을 먼저 실행합니다
🎜우리가 🎜특정 모듈을 사용할 때 해당 모듈의module.exports
만 가져오는 것이 아닙니다. 하지만 처음부터 실행합니다. module.exports = XXX
파일은 실제로는 한 줄의 코드일 뿐입니다. 나중에 설명하겠지만 이 코드 줄의 효과는 실제로 < 모듈의 code>export 속성입니다. 예를 들어 또 다른 c.js
를 살펴보겠습니다. 🎜MyModule._resolveFilename = function (request) { const filename = path.resolve(request); // 获取传入参数对应的绝对路径 const extname = path.extname(request); // 获取文件后缀名 // 如果没有文件后缀名,尝试添加.js和.json if (!extname) { const exts = Object.keys(MyModule._extensions); for (let i = 0; i < exts.length; i++) { const currentPath = `${filename}${exts[i]}`; // 如果拼接后的文件存在,返回拼接的路径 if (fs.existsSync(currentPath)) { return currentPath; } } } return filename; }
c.js
에서 우리는 c
를 내보냈는데, 이 c</ code> code>여러 단계의 계산 후 <code>module.exports = c;
행까지 실행하면 c
의 값은 2
입니다. 그래서 우리는 🎜 c.js
의 값은 2
입니다. c
의 값을 6
으로 변경해도 영향을 받지 않습니다. the Previous 이 코드 줄:🎜MyModule.prototype.load = function (filename) { // 获取文件后缀名 const extname = path.extname(filename); // 调用后缀名对应的处理函数来处理 MyModule._extensions[extname](this, filename); this.loaded = true; }
c.js
의 변수 c
는 기본 데이터 유형이므로 다음 c = 6;< /code>는 이전 <code>module.exports
에 영향을 미치지 않습니다. 참조 유형이면 어떻게 되나요? 직접 시도해 보겠습니다. 🎜MyModule._extensions['.js'] = function (module, filename) { const content = fs.readFileSync(filename, 'utf8'); module._compile(content, filename); }
index.js
에서 🎜He: 🎜module.exports = "hello world";
d.num이 <code>module.exports
다음에 제공되는 것을 발견했습니다. code>d
는 객체이자 참조 유형이고 이 참조를 통해 해당 값을 수정할 수 있으므로 할당은 여전히 유효합니다. 실제로 참조 유형의 경우 해당 값은 module.exports
뒤에서 수정될 수 있을 뿐만 아니라 모듈 외부에서도 수정될 수 있습니다. 예를 들어 index.js</code 내에서 직접 수정할 수 있습니다. > : 🎜<div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><pre class='brush:php;toolbar:false;'>function (module) { // 注入module变量,其实几个变量同理
module.exports = "hello world";
}</pre><div class="contentsignin">로그인 후 복사</div></div><div class="contentsignin">로그인 후 복사</div></div><h2 id="item-2">🎜 및 <code>module.exports
는 흑마법이 아닙니다 🎜🎜이전 예인 🎜 및 module.exports</에서 볼 수 있습니다. code >우리가 하는 일은 복잡하지 않습니다. 먼저 전역 개체 <code>{}
가 있다고 가정해 보겠습니다. 처음에는 파일을 생성할 때 파일을 꺼내서 실행합니다. this 파일에 module.exports
가 있습니다. 이 코드 줄을 실행하면 module.exports
값이 이 개체에 추가됩니다. 마지막으로 개체는 다음과 같습니다. 🎜MyModule.wrapper = [ '(function (exports, require, module, __filename, __dirname) { ', '\n});' ];
module.exports
를 추가합니다. 이 전역 개체를 추가하고 호출자에게 반환합니다. 이 전역 개체는 실제로 우리가 자주 듣는 캐시입니다. 따라서 🎜과 module.exports
사이에는 마법이 없습니다. 단지 실행하여 대상 파일의 값을 얻은 다음 캐시에 추가하고 필요할 때 꺼내기만 하면 됩니다. 이 개체를 다시 살펴보세요. d.js
는 참조 유형이므로 이 참조를 얻을 때마다 해당 값을 변경할 수 있습니다. Object.freeze()
, Object.defineProperty()
와 같은 메서드를 사용하는 등 모듈을 직접 작성할 때 변경 사항을 처리해야 합니다. 🎜모듈 유형 및 로딩 순서🎜🎜이 섹션의 내용은 다소 지루한 개념이지만 우리가 이해해야 할 내용이기도 합니다. 🎜모듈 유형
🎜Node.js 모듈에는 여러 유형이 있습니다. 우리가 이전에 사용한 것은 실제로 파일 모듈
입니다. , 주로 두 가지 유형이 있습니다: 🎜- 내장 모듈:
fs
, http
등과 같이 Node.js에서 기본적으로 제공하는 함수입니다. 이 모듈은 Node.js가 로드될 때 로드됩니다. .js 프로세스가 시작됩니다. fs
,http
等等,这些模块在Node.js进程起来时就加载了。- 文件模块:我们前面写的几个模块,还有第三方模块,即
node_modules
下面的模块都是文件模块。
加载顺序
- 내장 모듈:
fs
,http
등과 같이 Node.js에서 기본적으로 제공하는 함수입니다. 이 모듈은 Node.js가 로드될 때 로드됩니다. .js 프로세스가 시작됩니다. - 文件模块:我们前面写的几个模块,还有第三方模块,即
node_modules
下面的模块都是文件模块。
fs
,http
等等,这些模块在Node.js进程起来时就加载了。加载顺序是指当我们require(X)
时,应该按照什么顺序去哪里找X
,在官方文档上有详细伪代码,总结下来大概是这么个顺序:
- 优先加载内置模块,即使有同名文件,也会优先使用内置模块。
- 不是内置模块,先去缓存找。
- 缓存没有就去找对应路径的文件。
- 不存在对应的文件,就将这个路径作为文件夹加载。
- 对应的文件和文件夹都找不到就去
node_modules
下面找。- 还找不到就报错了。
加载文件夹
前面提到找不到文件就找文件夹,但是不可能将整个文件夹都加载进来,加载文件夹的时候也是有一个加载顺序的:
- 先看看这个文件夹下面有没有
package.json
,如果有就找里面的main
字段,main
字段有值就加载对应的文件。所以如果大家在看一些第三方库源码时找不到入口就看看他package.json
里面的main
字段吧,比如jquery
的main
字段就是这样:"main": "dist/jquery.js"
。- 如果没有
package.json
或者package.json
里面没有main
就找index
文件。- 如果这两步都找不到就报错了。
支持的文件类型
require
主要支持三种文件类型:
- .js:
.js
文件是我们最常用的文件类型,加载的时候会先运行整个JS文件,然后将前面说的module.exports
作为require
的返回值。- .json:
.json
文件是一个普通的文本文件,直接用JSON.parse
将其转化为对象返回就行。- .node:
.node
文件是C++编译后的二进制文件,纯前端一般很少接触这个类型。
手写require
前面其实我们已经将原理讲的七七八八了,下面来到我们的重头戏,自己实现一个require
。实现require
其实就是实现整个Node.js的模块加载机制,我们再来理一下需要解决的问题:
- 通过传入的路径名找到对应的文件。
- 执行找到的文件,同时要注入
module
和require
这些方法和属性,以便模块文件使用。- 返回模块的
module.exports
本文的手写代码全部参照Node.js官方源码,函数名和变量名尽量保持一致,其实就是精简版的源码,大家可以对照着看,写到具体方法时我也会贴上对应的源码地址。总体的代码都在这个文件里面:https://github.com/nodejs/node/blob/c6b96895cc74bc6bd658b4c6d5ea152d6e686d20/lib/internal/modules/cjs/loader.js
Module类
Node.js模块加载的功能全部在Module
类里面,整个代码使用面向对象的思想,如果你对JS的面向对象还不是很熟悉可以先看看这篇文章。Module
类的构造函数也不复杂,主要是一些值的初始化,为了跟官方Module
名字区分开,我们自己的类命名为MyModule
node_modules
아래의 모듈은 모두 파일 모듈입니다. 로딩 순서
로딩 순서는 require(X)
를 의미합니다. html#modules_all_together" rel="nofollow noreferrer" target="_blank">자세한 의사 코드</a>를 검색하면 요약하자면 대략 다음과 같은 순서입니다. 🎜🎜🎜🎜내장 모듈은 다음과 같습니다. 먼저 로드되며, 같은 이름의 파일이 있어도 내장 모듈이 먼저 사용됩니다. </li>🎜내장 모듈이 아니므로 먼저 캐시로 이동하여 찾으세요. </li>🎜캐시가 없다면 해당 경로의 파일을 찾아보세요. </li>🎜해당 파일이 없으면 이 경로가 폴더로 로드됩니다. </li>🎜해당 파일과 폴더를 찾을 수 없다면 <code>node_modules
로 이동하여 찾아보세요. 🎜찾을 수 없어서 오류신고를 했는데요.
폴더 불러오기
앞서 말씀드린 것처럼 파일을 찾을 수 없다면 그냥 폴더를 찾아보세요, 폴더 전체를 로드하는 것은 불가능합니다. 폴더를 로드할 때 로드 순서도 있습니다. 🎜🎜🎜🎜먼저 이 폴더 아래에 package.json
이 있는지 확인하세요. < 내부 코드>기본 필드를 찾아 기본
필드에 값이 있으면 해당 파일을 로드합니다. 따라서 일부 타사 라이브러리의 소스 코드를 볼 때 입구를 찾을 수 없는 경우 package.json
에서 main
필드를 살펴보세요. jquery
main
필드는 "main": "dist/jquery.js"
와 같습니다. 🎜package.json
이 없거나 package.json
에 main
이 없으면 index를 찾으세요.
파일. 🎜이 두 단계 중 어느 것도 찾을 수 없으면 오류가 보고됩니다.
지원되는 파일 형식
require
는 주로 세 가지 파일 형식을 지원합니다. 🎜🎜 🎜🎜🎜.js🎜: .js
파일은 가장 일반적으로 사용되는 파일 형식입니다. 로드 시 전체 JS 파일이 먼저 실행된 다음 module.exports
가 실행됩니다. 위에서 언급한 code>가 require
의 반환 값으로 실행됩니다. 🎜🎜.json🎜: .json
파일은 일반 텍스트 파일입니다. JSON.parse
를 사용하여 객체로 변환하고 반환하면 됩니다. 🎜🎜.node🎜: .node
파일은 C++로 컴파일된 바이너리 파일입니다. 순수 프런트 엔드는 일반적으로 이 유형과 거의 접촉하지 않습니다.
손으로 쓴 require
사실 앞서 이미 원칙에 대해 자세히 설명했습니다. 자, 이제 우리의 하이라이트인 require
를 직접 구현해 보겠습니다. require
를 구현하는 것은 실제로 Node.js 전체의 모듈 로딩 메커니즘을 구현하는 것입니다. 해결해야 할 문제를 살펴보겠습니다. 🎜🎜🎜🎜 들어오는 경로 이름을 통해 해당 파일을 찾습니다. 🎜찾은 파일을 실행하는 동시에 모듈 파일을 사용할 수 있도록 module
및 require
메서드와 속성을 삽입합니다. 🎜모듈의 module.exports
로 돌아가기
이 글의 손으로 쓴 코드는 모두 공식 Node.js 소스 코드를 참조합니다. , 그리고 함수명과 변수명은 최대한 비슷하게 작성하여 일관성을 유지하기 위해 실제로는 소스코드를 단순화한 버전이므로 구체적인 메소드를 적어주시면 해당 소스코드도 함께 올려드리겠습니다. 주소. 전체 코드는 다음 파일에 있습니다: https://github.com/nodejs/node/blob/c6b96895cc74bc6bd658b4c6d5ea152d6e686d20/lib/internal/modules/cjs/loader.js🎜
모듈 클래스 < /h3>
Node.js 모듈 로딩 기능은 모두 Module
클래스에 있습니다. 전체 코드는 객체 지향적 사고(JS 객체 지향에 익숙하지 않다면 먼저 이 글을 읽어보세요. Module
클래스의 생성자도 복잡하지 않습니다. 주로 일부 값을 초기화합니다. 공식 Module
이름과 구별하기 위해 자체 클래스 이름을 로 지정합니다. >내모듈< /code>:🎜<div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><pre class='brush:php;toolbar:false;'>function MyModule(id = &#39;&#39;) {
this.id = id; // 这个id其实就是我们require的路径
this.path = path.dirname(id); // path是Node.js内置模块,用它来获取传入参数对应的文件夹路径
this.exports = {}; // 导出的东西放这里,初始化为空对象
this.filename = null; // 模块对应的文件名
this.loaded = false; // loaded用来标识当前模块是否已经加载
}</pre><div class="contentsignin">로그인 후 복사</div></div><div class="contentsignin">로그인 후 복사</div></div><h3 id="require方法">require方法</h3><p>我们一直用的<code>require
其实是Module
类的一个实例方法,内容很简单,先做一些参数检查,然后调用Module._load
方法,源码看这里:https://github.com/nodejs/node/blob/c6b96895cc74bc6bd658b4c6d5ea152d6e686d20/lib/internal/modules/cjs/loader.js#L970。精简版的代码如下:
MyModule.prototype.require = function (id) { return Module._load(id); }
MyModule._load
MyModule._load
是一个静态方法,这才是require
方法的真正主体,他干的事情其实是:
- 先检查请求的模块在缓存中是否已经存在了,如果存在了直接返回缓存模块的
exports
。- 如果不在缓存中,就
new
一个Module
实例,用这个实例加载对应的模块,并返回模块的exports
。
我们自己来实现下这两个需求,缓存直接放在Module._cache
这个静态变量上,这个变量官方初始化使用的是Object.create(null)
,这样可以使创建出来的原型指向null
,我们也这样做吧:
MyModule._cache = Object.create(null); MyModule._load = function (request) { // request是我们传入的路劲参数 const filename = MyModule._resolveFilename(request); // 先检查缓存,如果缓存存在且已经加载,直接返回缓存 const cachedModule = MyModule._cache[filename]; if (cachedModule !== undefined) { return cachedModule.exports; } // 如果缓存不存在,我们就加载这个模块 // 加载前先new一个MyModule实例,然后调用实例方法load来加载 // 加载完成直接返回module.exports const module = new MyModule(filename); // load之前就将这个模块缓存下来,这样如果有循环引用就会拿到这个缓存,但是这个缓存里面的exports可能还没有或者不完整 MyModule._cache[filename] = module; module.load(filename); return module.exports; }
上述代码对应的源码看这里:https://github.com/nodejs/node/blob/c6b96895cc74bc6bd658b4c6d5ea152d6e686d20/lib/internal/modules/cjs/loader.js#L735
可以看到上述源码还调用了两个方法:MyModule._resolveFilename
和MyModule.prototype.load
,下面我们来实现下这两个方法。
MyModule._resolveFilename
MyModule._resolveFilename
从名字就可以看出来,这个方法是通过用户传入的require
参数来解析到真正的文件地址的,源码中这个方法比较复杂,因为按照前面讲的,他要支持多种参数:内置模块,相对路径,绝对路径,文件夹和第三方模块等等,如果是文件夹或者第三方模块还要解析里面的package.json
和index.js
。我们这里主要讲原理,所以我们就只实现通过相对路径和绝对路径来查找文件,并支持自动添加js
和json
两种后缀名:
MyModule._resolveFilename = function (request) { const filename = path.resolve(request); // 获取传入参数对应的绝对路径 const extname = path.extname(request); // 获取文件后缀名 // 如果没有文件后缀名,尝试添加.js和.json if (!extname) { const exts = Object.keys(MyModule._extensions); for (let i = 0; i < exts.length; i++) { const currentPath = `${filename}${exts[i]}`; // 如果拼接后的文件存在,返回拼接的路径 if (fs.existsSync(currentPath)) { return currentPath; } } } return filename; }
上述源码中我们还用到了一个静态变量MyModule._extensions
,这个变量是用来存各种文件对应的处理方法的,我们后面会实现他。
MyModule._resolveFilename
对应的源码看这里:https://github.com/nodejs/node/blob/c6b96895cc74bc6bd658b4c6d5ea152d6e686d20/lib/internal/modules/cjs/loader.js#L822
MyModule.prototype.load
MyModule.prototype.load
是一个实例方法,这个方法就是真正用来加载模块的方法,这其实也是不同类型文件加载的一个入口,不同类型的文件会对应MyModule._extensions
里面的一个方法:
MyModule.prototype.load = function (filename) { // 获取文件后缀名 const extname = path.extname(filename); // 调用后缀名对应的处理函数来处理 MyModule._extensions[extname](this, filename); this.loaded = true; }
注意这段代码里面的this
指向的是module
实例,因为他是一个实例方法。对应的源码看这里: https://github.com/nodejs/node/blob/c6b96895cc74bc6bd658b4c6d5ea152d6e686d20/lib/internal/modules/cjs/loader.js#L942
加载js文件: MyModule._extensions['.js']
前面我们说过不同文件类型的处理方法都挂载在MyModule._extensions
上面的,我们先来实现.js
类型文件的加载:
MyModule._extensions['.js'] = function (module, filename) { const content = fs.readFileSync(filename, 'utf8'); module._compile(content, filename); }
可以看到js
的加载方法很简单,只是把文件内容读出来,然后调了另外一个实例方法_compile
来执行他。对应的源码看这里:https://github.com/nodejs/node/blob/c6b96895cc74bc6bd658b4c6d5ea152d6e686d20/lib/internal/modules/cjs/loader.js#L1098
编译执行js文件:MyModule.prototype._compile
MyModule.prototype._compile
是加载JS文件的核心所在,也是我们最常使用的方法,这个方法需要将目标文件拿出来执行一遍,执行之前需要将它整个代码包裹一层,以便注入exports, require, module, __dirname, __filename
,这也是我们能在JS文件里面直接使用这几个变量的原因。要实现这种注入也不难,假如我们require
的文件是一个简单的Hello World
,长这样:
module.exports = "hello world";
那我们怎么来给他注入module
这个变量呢?答案是执行的时候在他外面再加一层函数,使他变成这样:
function (module) { // 注入module变量,其实几个变量同理 module.exports = "hello world"; }
所以我们如果将文件内容作为一个字符串的话,为了让他能够变成上面这样,我们需要再给他拼接上开头和结尾,我们直接将开头和结尾放在一个数组里面:
MyModule.wrapper = [ '(function (exports, require, module, __filename, __dirname) { ', '\n});' ];
注意我们拼接的开头和结尾多了一个()
包裹,这样我们后面可以拿到这个匿名函数,在后面再加一个()
就可以传参数执行了。然后将需要执行的函数拼接到这个方法中间:
MyModule.wrap = function (script) { return MyModule.wrapper[0] + script + MyModule.wrapper[1]; };
这样通过MyModule.wrap
包装的代码就可以获取到exports, require, module, __filename, __dirname
这几个变量了。知道了这些就可以来写MyModule.prototype._compile
了:
MyModule.prototype._compile = function (content, filename) { const wrapper = Module.wrap(content); // 获取包装后函数体 // vm是nodejs的虚拟机沙盒模块,runInThisContext方法可以接受一个字符串并将它转化为一个函数 // 返回值就是转化后的函数,所以compiledWrapper是一个函数 const compiledWrapper = vm.runInThisContext(wrapper, { filename, lineOffset: 0, displayErrors: true, }); // 准备exports, require, module, __filename, __dirname这几个参数 // exports可以直接用module.exports,即this.exports // require官方源码中还包装了一层,其实最后调用的还是this.require // module不用说,就是this了 // __filename直接用传进来的filename参数了 // __dirname需要通过filename获取下 const dirname = path.dirname(filename); compiledWrapper.call(this.exports, this.exports, this.require, this, filename, dirname); }
上述代码要注意我们注入进去的几个参数和通过call
传进去的this
:
- this:
compiledWrapper
是通过call
调用的,第一个参数就是里面的this
,这里我们传入的是this.exports
,也就是module.exports
,也就是说我们js
文件里面this
是对module.exports
的一个引用。- exports:
compiledWrapper
正式接收的第一个参数是exports
,我们传的也是this.exports
,所以js
文件里面的exports
也是对module.exports
的一个引用。- require: 这个方法我们传的是
this.require
,其实就是MyModule.prototype.require
,也就是MyModule._load
。- module: 我们传入的是
this
,也就是当前模块的实例。- __filename:文件所在的绝对路径。
- __dirname: 文件所在文件夹的绝对路径。
到这里,我们的JS文件其实已经记载完了,对应的源码看这里:https://github.com/nodejs/node/blob/c6b96895cc74bc6bd658b4c6d5ea152d6e686d20/lib/internal/modules/cjs/loader.js#L1043
加载json文件: MyModule._extensions['.json']
加载json
文件就简单多了,只需要将文件读出来解析成json
就行了:
MyModule._extensions['.json'] = function (module, filename) { const content = fs.readFileSync(filename, 'utf8'); module.exports = JSONParse(content); }
exports
和module.exports
的区别
网上经常有人问,node.js
里面的exports
和module.exports
到底有什么区别,其实前面我们的手写代码已经给出答案了,我们这里再就这个问题详细讲解下。exports
和module.exports
这两个变量都是通过下面这行代码注入的。
compiledWrapper.call(this.exports, this.exports, this.require, this, filename, dirname);
初始状态下,exports === module.exports === {}
,exports
是module.exports
的一个引用,如果你一直是这样使用的:
exports.a = 1; module.exports.b = 2; console.log(exports === module.exports); // true
上述代码中,exports
和module.exports
都是指向同一个对象{}
,你往这个对象上添加属性并没有改变这个对象本身的引用地址,所以exports === module.exports
一直成立。
但是如果你哪天这样使用了:
exports = { a: 1 }
或者这样使用了:
module.exports = { b: 2 }
那其实你是给exports
或者module.exports
重新赋值了,改变了他们的引用地址,那这两个属性的连接就断开了,他们就不再相等了。需要注意的是,你对module.exports
的重新赋值会作为模块的导出内容,但是你对exports
的重新赋值并不能改变模块导出内容,只是改变了exports
这个变量而已,因为模块始终是module
,导出内容是module.exports
。
循环引用
Node.js对于循环引用是进行了处理的,下面是官方例子:
a.js
:
console.log('a 开始'); exports.done = false; const b = require('./b.js'); console.log('在 a 中,b.done = %j', b.done); exports.done = true; console.log('a 结束');
b.js
:
console.log('b 开始'); exports.done = false; const a = require('./a.js'); console.log('在 b 中,a.done = %j', a.done); exports.done = true; console.log('b 结束');
main.js
:
console.log('main 开始'); const a = require('./a.js'); const b = require('./b.js'); console.log('在 main 中,a.done=%j,b.done=%j', a.done, b.done);
当 main.js
加载 a.js
时, a.js
又加载 b.js
。 此时, b.js
会尝试去加载 a.js
。 为了防止无限的循环,会返回一个 a.js
的 exports
对象的 未完成的副本 给 b.js
模块。 然后 b.js
完成加载,并将 exports
对象提供给 a.js
模块。
那么这个效果是怎么实现的呢?答案就在我们的MyModule._load
源码里面,注意这两行代码的顺序:
MyModule._cache[filename] = module; module.load(filename);
上述代码中我们是先将缓存设置了,然后再执行的真正的load
,顺着这个思路我能来理一下这里的加载流程:
main
加载a
,a
在真正加载前先去缓存中占一个位置a
在正式加载时加载了b
b
又去加载了a
,这时候缓存中已经有a
了,所以直接返回a.exports
,即使这时候的exports
是不完整的。
总结
-
require
不是黑魔法,整个Node.js的模块加载机制都是JS
实现的。 - 每个模块里面的
exports, require, module, __filename, __dirname
五个参数都不是全局变量,而是模块加载的时候注入的。 - 为了注入这几个变量,我们需要将用户的代码用一个函数包裹起来,拼一个字符串然后调用沙盒模块
vm
来实现。 - 初始状态下,模块里面的
this, exports, module.exports
都指向同一个对象,如果你对他们重新赋值,这种连接就断了。 - 对
module.exports
的重新赋值会作为模块的导出内容,但是你对exports
的重新赋值并不能改变模块导出内容,只是改变了exports
这个变量而已,因为模块始终是module
,导出内容是module.exports
。 - 为了解决循环引用,模块在加载前就会被加入缓存,下次再加载会直接返回缓存,如果这时候模块还没加载完,你可能拿到未完成的
exports
。 - Node.js实现的这套加载机制叫CommonJS。
本文完整代码已上传GitHub:https://github.com/dennis-jiang/Front-End-Knowledges/blob/master/Examples/Node.js/Module/MyModule/index.js
参考资料
Node.js模块加载源码:https://github.com/nodejs/node/blob/c6b96895cc74bc6bd658b4c6d5ea152d6e686d20/lib/internal/modules/cjs/loader.js
Node.js模块官方文档:http://nodejs.cn/api/modules.html
文章的最后,感谢你花费宝贵的时间阅读本文,如果本文给了你一点点帮助或者启发,请不要吝啬你的赞和GitHub小星星,你的支持是作者持续创作的动力。
作者博文GitHub项目地址: https://github.com/dennis-jiang/Front-End-Knowledges
更多编程相关知识,可访问:编程教学!!
위 내용은 Node.js의 모듈 로딩 메커니즘에 대한 심층 분석의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

핫 AI 도구

Undresser.AI Undress
사실적인 누드 사진을 만들기 위한 AI 기반 앱

AI Clothes Remover
사진에서 옷을 제거하는 온라인 AI 도구입니다.

Undress AI Tool
무료로 이미지를 벗다

Clothoff.io
AI 옷 제거제

Video Face Swap
완전히 무료인 AI 얼굴 교환 도구를 사용하여 모든 비디오의 얼굴을 쉽게 바꾸세요!

인기 기사

뜨거운 도구

메모장++7.3.1
사용하기 쉬운 무료 코드 편집기

SublimeText3 중국어 버전
중국어 버전, 사용하기 매우 쉽습니다.

스튜디오 13.0.1 보내기
강력한 PHP 통합 개발 환경

드림위버 CS6
시각적 웹 개발 도구

SublimeText3 Mac 버전
신 수준의 코드 편집 소프트웨어(SublimeText3)

뜨거운 주제











이 기사는 NodeJS V8 엔진의 메모리 및 가비지 수집기(GC)에 대한 심층적인 이해를 제공할 것입니다. 도움이 되기를 바랍니다.

Non-Blocking, Event-Driven 기반으로 구축된 Node 서비스는 메모리 소모가 적다는 장점이 있으며, 대규모 네트워크 요청을 처리하는데 매우 적합합니다. 대규모 요청을 전제로 '메모리 제어'와 관련된 문제를 고려해야 합니다. 1. V8의 가비지 수집 메커니즘과 메모리 제한 Js는 가비지 수집 기계에 의해 제어됩니다.

Node용 Docker 이미지를 선택하는 것은 사소한 문제처럼 보일 수 있지만 이미지의 크기와 잠재적인 취약점은 CI/CD 프로세스와 보안에 상당한 영향을 미칠 수 있습니다. 그렇다면 최고의 Node.js Docker 이미지를 어떻게 선택합니까?

Node 19가 정식 출시되었습니다. 이 글에서는 Node.js 19의 6가지 주요 기능에 대해 자세히 설명하겠습니다. 도움이 되셨으면 좋겠습니다!

파일 모듈은 파일 읽기/쓰기/열기/닫기/삭제 추가 등과 같은 기본 파일 작업을 캡슐화한 것입니다. 파일 모듈의 가장 큰 특징은 모든 메소드가 **동기** 및 ** 두 가지 버전을 제공한다는 것입니다. 비동기**, sync 접미사가 있는 메서드는 모두 동기화 메서드이고, 없는 메서드는 모두 이기종 메서드입니다.

Node.js는 GC(가비지 수집)를 어떻게 수행하나요? 다음 기사에서는 이에 대해 설명합니다.

이벤트 루프는 Node.js의 기본 부분이며 메인 스레드가 차단되지 않도록 하여 비동기 프로그래밍을 가능하게 합니다. 이벤트 루프를 이해하는 것은 효율적인 애플리케이션을 구축하는 데 중요합니다. 다음 기사는 Node.js의 이벤트 루프에 대한 심층적인 이해를 제공할 것입니다. 도움이 되기를 바랍니다!

초기에 JS는 브라우저 측에서만 실행되었습니다. 유니코드로 인코딩된 문자열은 처리하기 쉬웠지만 바이너리 및 유니코드가 아닌 인코딩된 문자열을 처리하는 것은 어려웠습니다. 그리고 바이너리는 컴퓨터의 가장 낮은 데이터 형식인 비디오/오디오/프로그램/네트워크 패키지입니다.
