本文你能学到什么
-
自己手写实现一个
require
,面试用也可以。 -
如何看
Node.js
源码 -
require
函数是如何产生的?为什么在module
中可以直接使用。 -
require
加载原生模块时候如何处理的,为什么require('net')
可以直接找到 -
Node.js
中require
会出现循环引用问题吗? -
require
是同步还是异步的?为什么? -
exports
和module.exports
的区别是什么? -
你知道
require
加载的过程中使用了vm
模块吗?vm
模块是做什么的?vm
模块除了require
源码用到还有哪些应用场景。
请注意我上面提出的问题,本文学完后看看是否都搞能懂。最好跟着练习一遍手写
require
源码,美滋滋。
什么是 CommonJS
每一个文件就是一个模块,拥有自己独立的作用域,变量,以及方法等,对其他的模块都不可见。CommonJS
规范规定,每个模块内部,module
变量代表当前模块。这个变量是一个对象,它的exports
属性(即module.exports
)是对外的接口。
模块分类
-
原生(核心)模块:
Node
提供的模块我们都称之为原生模块-
内建模块:
Node.js
原生提供的模块中,由纯C/C++
编写的称为内建模块 -
全局模块:
Node.js
在启动时,会生成一个全局量process
-
除了上面两种可以直接
require
的所有原生模块
-
-
文件模块:用户编写的模块
-
普通文件模块:
node_modules
下面的模块,或者我们自己开发时候写的每个文件内容。 -
C++
扩展模块:用户自己编写的C++
扩展模块或者第三方C++
扩展模块
-
模块加载
介绍了上面的模块分类,正常应该到介绍不同模块的加载环节,这里不这样,只列出目录。先带你看一遍源码,再手写一下,然后我想你自己总结一下这几种模块的加载区别。
加载 Node.js 原生模块
本文不包括直接调用内建纯
C/C++
模块,也不推荐这样使用,因为我们正常调用的原生模块都是通过js
封装一层,它们自己再去调用,你想直接调用的Node.js
提供的存C/C++
内建模块,js
封装的一层也都能做到。那部分内容放在Node.js
与C++
那些事的文章中介绍。
require 加载普通文件模块
require 加载 C++ 扩展文件模块
require 加载原理(源码分析与手写)
![](https://i-blog.csdnimg.cn/blog_migrate/6733f699eaac1a0bed6ab99aca9e5aed.png)
画了一个源码导图,可以直接跟着导图学一遍配上文章讲解,效果更佳。(导图太大了上传不清晰,需要找我要吧。)
require
源码并不复杂,这里采用的是边看源码边手写的方式讲解(我们最终实现的require
是简易版本,一些源码提到,但是简易版本不会实现),实现 require
其实就是实现整个 Node.js
的模块加载机制,Node.js
的模块加载机制总结下来一共八步。
网上一些文章只分成了3-4步,我这里做了一下细化,为了彻底搞懂我开篇提到的一些问题。
1. 基础准备阶段
Node.js
模块加载的主流程都在 Module
类中,在源码的https://github.com/nodejs/node/blob/master/lib/internal/modules/cjs/loader.js#L150
中进行了基础 Module
类定义,这个构造函数中的内容主要做一些值的初始化,我们自己对照着实现下,为了和源码有一个区别,本文使用 KoalaModule
命名。
function KoalaModule(id = '') {
this.id = id; // 这个id其实就是我们require的路径
this.path = path.dirname(id); // path是Node.js内置模块,用它来获取传入参数对应的文件夹路径
this.exports = {}; // 导出的东西放这里,初始化为空对象
this.filename = null; // 模块对应的文件名
this.loaded = false; // loaded用来标识当前模块是否已经加载
}
KoalaModule._cache = Object.create(null); //创建一个空的缓存对象
KoalaModule._extensions = Object.create(null); // 创建一个空的扩展点名类型函数对象(后面会知道用来做什么)
然后在源码中你会找到 require
函数,在 KoalaModule
的原型链上,我们实现下
Module.prototype.require = function(id) {
return Module._load(id, this, /* isMain */ false);
};
在源码中你会发现又调用了_load
函数,找到源码中的 _load
函数,(源码位置:https://github.com/nodejs/node/blob/master/lib/internal/modules/cjs/loader.js#L724
)下面的所有步骤都是在这个函数中完成调用和 return
的,实现简易版_load
函数。
KoalaModule._load = function (request) { // request是我们传入的路劲参数
// 2.路径分析并定位到文件
const filename = KoalaModule._resolveFilename(request);
// 3.判断模块是否加载过(缓存判断)
const cachedModule = koalaModule._cache[filename];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
// 4. 去加载 node 原生模块中
/*const mod = loadNativeModule(filename, request);
if (mod && mod.canBeRequiredByUsers) return mod.exports;*/