console.log('main starting');
var a = require('./a.js'); // --> 0
var b = require('./b.js');
console.log('in main, a.done=%j, b.done=%j', a.done, b.done);
Module.runMain = function() {
// Load the main module--the command line argument.
Module._load(process.argv[1], null, true);
// Handle any nextTicks added in the first tick of the program
process._tickCallback();
};
在runMain方法中调用了_load方法:
Module._load = function(request, parent, isMain) {
var filename = Module._resolveFilename(request, parent);
var cachedModule = Module._cache[filename];
if (cachedModule) {
return cachedModule.exports;
}
var module = new Module(filename, parent);
Module._cache[filename] = module;
module.load(filename);
return module.exports;
};
Node.js 中的循环依赖
我们在写node的时候有可能会遇到循环依赖的情况,什么是循环依赖,怎么避免或解决循环依赖问题?
先看一段官网给出的循环依赖的代码:
a.js
:b.js
:main.js
:如果我们启动
main.js
会出现什么情况? 在a.js
中加载b.js
,然后在b.js
中加载a.js
,然后再在a.js
中加载b.js
吗?这样就会造成循环依赖死循环。让我们执行看看:
可以看到程序并没有陷入死循环,从上面的执行结果可以看到
main.js
中先require
了a.js
,a.js
中执行完了console
和export.done=fasle
之后,转而去加载b.js
,待b.js
被load完之后,再返回a.js
中执行完剩下的代码。我在官网的代码基础上增加了一些注释,基本 load 顺序就是按照这个
0-->1-->2-->3-->4
的顺序去执行的,然后在第二步下面我打印出了require('./a')
的结果,可以看到是{done:false}
,可以猜测在b.js
中require('./a')
的结果是a.js
中已经执行到的exports
出的值。上面所说的还只是基于结果基础上的猜测,没有什么说服力,为了验证我的猜测是正确的,我把 Node 的源码稍微翻看了一些,C++ 的代码看不懂没关系,能看懂 JS 的部分就可以了,下面就是 Node 源码的分析(主要是 module 的分析, Node 源码在此):
将会分析的主要源码:
启动 $ node main.js
C++ 的代码我看不懂,总而言之,在我查了资料之后知道当我们在
shell
中输入node main.js
之后,会先执行node/src/node.cc
,然后会执行node/src/node.js
, 所以C++代码不分析,从分析node/src/node.js
开始(只会分析和主题相关的代码)。node.js 源码分析
node.js
文件主要结构为这种闭包代码很常见,从名字可以看出,此处为启动文件。接下来看看 startup 函数中有一大块条件语句,我删除大多数无关代码,如下:
我把无关的代码基本都删除了。可以看到这段代码主要做的事是先通过 Native 引入
module
模块,执行Module.runMain()
。很多人都知道
require
核心代码,如 require('path'),不需要写全路径,Node 是怎样做到的呢?上面大段介绍基本引自朴老师的「深入浅出 Node.js」。大概理解就是在启动命令的时候,Node 会把
node.js
和lib/*.js
的内容都放到process
中传入当前闭包中,我们在当前函数就可以通过process.binding('natives')
取出来放到 _source 中,如下代码所示:接下来看看
NativeModule.require
做了哪些事情:这上面的代码表明内建模块被缓存,就直接返回内建模块的
exports
,如果没有的话,就生成一个核心模块的实例,然后先把模块根据id来cache
,然后调用nativeModule.compile
接口编译源文件:cache 是把实例根据 id 放到 _cache 对象中。先从 _source 中取出对应id的源文件字符串,包上一层
(function (exports, require, module, __filename, __dirname) {\n', '\n});
。比如main.js
最终变成如下JS代码的字符串:runInThisContext
是将被包装后的源字符串转成可执行函数,(runInThisContext
来自contextify
模块),runInThisContext
的作用,类似eval
,再执行这个被eval
后的函数,就算被 load 完成了,最后把 load 设为 true。可以看到
fn
的实参为this.exports; NativeModule.require; this; this.filename;
。所以
require('module')
的作用是加载/lib/module.js
文件。让我们再回到 startup 函数,加载完 module.js,紧接着运行Module.runMain()
方法。(估计有人忘了前面的startup函数是干嘛的,我再放一次,省得再拉回去了)module.js源码分析
上面走完了
NatvieModule
的加载代码。再看看module.js
是怎样加载用户使用的文件的。这是
Module
的构造函数,Module.wrapper
和Module.wrap
,是由NativeModule
赋值来的,Module._cache
是个空对象,存放所有被 load 后的模块 id。在
node.js
文件的 startup 函数中,最后一步走到Module.runMain()
:在
runMain
方法中调用了_load
方法:上述代码照例我删除了一些不是很相关的代码,从剩下的代码可以看出
_load
函数的主要干了两件事(还有一件加载NativeModule的代码被我删掉了):在
load
方法中判断源文件的扩展名是什么,默认是'.js'
,(我这里也只分析后缀是.js
的情况),然后调用Module._extensions[extension]()
方法,并传入 this 和 filename;当extension
是'.js'
的时候, 调用Module._extensions['.js']()
方法。这个方法是读到源文件的字符串后,调用
module._compile
方法。其实跟
NativeModule
的_complie
做的事情差不多。先把源文件content
包装一层(function (exports, require, module, __filename, __dirname) {\n', '\n});
, 然后通过runInThisContext
把字符串转成可执行的函数,最后把self.exports, require, self, filename, dirname
这几个实参传入可执行函数中。require
方法为:循环依赖的时候为什么不会无限循环引用
所谓的循环依赖就是在两个不同的文件中互相应用了对方。假设按照最上面官网给出的例子中,
在
main.js
中:require('./a.js')
;此时会调用self.require()
, 然后会走到module._load
,在_load
中会判断./a.js
是否被load过,当然运行到这里,./a.js
还没被 load 过,所以会走完整个load流程,直到_compile
。./a.js
,运行到exports.done = false
的时候,给 esports 增加了一个属性。此时的exports={done: false}
。require('./b.js')
,同 第 1 步。./b.js
,到require('./a.js')
。此时走到_load
函数的时候发现./a.js
已经被load过了,所以会直接从_cache
中返回。所以此时./a.js
还没有运行完,exports = {done.false}
,那么返回的结果就是in b, a.done = false
;./b.js
全部运行完毕,回到./a.js
中,继续向下运行,此时的./b.js
的exports={done:true}
, 结果自然是in main, a.done=true, b.done=true