require的实现原理
几乎所有Node.js开发人员都可以告诉你这个require()函数的作用,但是我们中有多少人真的知道它是如何工作的?我们每天都使用它来加载库和模块,但它的行为却是一个谜。好奇,我挖掘了Node核心,了解幕后发生了什么。但是,我没有找到单一功能,而是成为Node模块系统的核心:module.js。该文件包含一个令人惊讶的功能但相对未知的核心模块,它控制所使用的每个文件的加载,编译和缓存。require()事实证明,这只是冰山一角。
MODULE.JS
1 | function Module(id, parent) { |
找到的Module类型在module.jsNode.js 中有两个主要角色。首先,它为所有Node.js模块的构建提供了基础。每个文件在加载时都会获得此基本模块的新实例,即使在文件运行后也会持续存在。这就是为什么我们可以module.exports根据需要附加属性并在以后返回它们。
该模块的第二项重要工作是处理Node的模块加载机制。require我们使用的独立函数实际上是一个抽象module.require,它本身只是一个简单的包装器Module._load。这种加载方法处理每个文件的实际加载,是我们开始旅程的地方。
Module._load
1 | Module._load = function(request, parent, isMain) { |
Module._load负责加载新模块和管理模块缓存。在加载时缓存每个模块可减少冗余文件读取的数量,并可显着加快应用程序的速度。此外,共享模块实例允许类似单一模块,可以在项目中保持状态。
如果缓存中不存在模块,Module._load则将为该文件创建新的基本模块。然后它会告诉模块在发送新文件之前读入新文件的内容module._compile。
如果您注意到上面的步骤#6,您将看到module.exports返回给用户。这就是你使用exports和module.exports定义公共接口的原因,因为那正是Module._load然后require会返回的。令我感到惊讶的是,这里没有更多的魔法,但如果有什么更好的话。
module._compile
1 | Module.prototype._compile = function(content, filename) { |
这是真正的魔法发生的地方。首先,require为该模块创建一个特殊的独立功能。这是我们都熟悉的必需功能。虽然函数本身只是一个包装器Module.require,但它还包含一些鲜为人知的帮助器属性和方法供我们使用:
- require():加载外部模块
- require.resolve():将模块名称解析为其绝对路径
- require.main:主要模块
- require.cache:所有缓存的模块
- require.extensions:每种有效文件类型的可用编译方法,基于其扩展名
一旦require准备好,整个装载的源代码被包裹在一个新的功能,这需要在 require,module,exports,和所有其他暴露变量作为自变量。这为该模块创建了一个新的功能范围,因此不会污染Node.js的其余环境。
1 |
|
最后,运行包装模块的功能。整个Module._compile方法是同步执行的,因此原始调用Module._load只是在完成并返回module.exports给用户之前等待此代码运行。
####结论
因此,我们已经到达了需求代码路径的末尾,并且通过创建require我们首先开始调查的函数,这样做已经完全循环。
如果你已经完成了这一切,那么你已经准备好了最后的秘密:require(‘module’)。没错,模块系统本身可以通过模块系统加载。盗梦空间。这可能听起来很奇怪,但它允许userland模块与加载系统交互而无需深入Node.js核心。像嘲弄和重新布线这样的流行模块都是基于此构建的。
评论