前言
在朴灵老师的《深入浅出nodejs》一书中提到,每个模块文件的require,exports和module这3个变量并没有在模块中定义,也并非全局函数/对象。而是在编译的时候Node对js文件内容进行了头尾的包装。在头部加了(function (exports, require, module, __filename, __dirname) {,在尾部加了 \n});。这样看起来虽然貌似理解了require,exports和module的来源。但是依然不明白在这个函数外调用时,比如require时,发生了什么。于是花了点时间去仔细阅读了一下node的源代码,感觉豁然开朗,这里把我的分析过程记录一下,仅供大家参考。
源文件分析
有关require的内容,只要阅读两个文件就够了。分别是lib/internal/bootstrap_node.js和lib/module.js。
1、require入口:
可以看到,这里继续调用了_load方法用于加载文件
2、Module._load方法:
可以看到,这里继续调用了_resolveFilename方法,顾名思义,这应该是一个解析需要require的文件名的方法
3、Module._resolveFilename方法:
可以看到,这里又继续调用了_findPath方法
4、Module._findPath方法:
可以看到,这里完整的显示了node是如何根据require传入的名称来定位具体的文件的,他们的顺序依次是:
1、先从缓存中读取,如果没有则继续往下
2、判断需要模块路径是否以/结尾,如果不是,则要判断
a. 检查是否是一个文件,如果是,则转换为真实路径
b. 否则如果是一个目录,则调用tryPackage方法读取该目录下的package.json文件,把里面的main属性设置为filename
c. 如果没有读到路径上的文件,则通过tryExtensions尝试在该路径后依次加上.js,.json和.node后缀,判断是否存在,若存在则返回加上后缀后的路径
3、如果依然不存在,则同样调用tryPackage方法读取该目录下的package.json文件,把里面的main属性设置为filename
4、如果依然不存在,则尝试在该路径后依次加上index.js,index.json和index.node,判断是否存在,若存在则返回拼接后的路径。
5、若解析成功,则把解析得到的文件名cache起来,下次require就不用再次解析了,否则若解析失败,则返回false
5、文件名成功获取后,再次回到Module._load方法:
可以看到,这里继续调用了load方法来加载文件
6、Module.load方法:
可以看到,这里将根据不同的文件类型(js,json和node),采用不同的加载方法
7、不同文件类型的加载方法不同:
可以看到,js文件将在读入文件(同步读)内容后进行编译,json文件则用JSON.parse解析内容,node文件则使用dlopen进行动态链接库载入
8、这里仅针对通常的js类型的文件的载入进行分析:
可以看到,js文件内容先被wrap(包装)了一下,然后才使用runInThisContext来运行包装后的代码,而传入的参数就是前面说的exports, require, module,还有当前文件名及所在目录名。此外,也看到,模块中的this其实是指向module的exports,而不是global!
9、Module.wrap:
可以看到,Module.wrap只是NativeModule.wrap的引用,这里的NativeModule则位于lib/internal/bootstrap_node.js中
10、NativeModule.wrap:
可以看到,这里就对上了文章开头所说的编译时node文件内容的头尾包装,自此,本次源码分析结果。
总结
从上面的分析可以看出来,exports其实是module的属性,require则是Module原型的方法。exports.xx=xx,其实跟module.exports.xx=xx其实是一样的,不过如果直接为export赋值,则不能写成exports=xx,而应该写成module.exports=xx,因为exports在这里只是一个引用。
从上面也可以看到,每一次require,都会把new一个Module,并且把这个Module添加到当前模块的children中,并且返回新建的Module对象的exports。
其实node启动的原理跟require是一样的,src/node.cc中的node::LoadEnvironment函数会被调用,在该函数内则会接着调用lib/internal/bootstrap_node.js中的代码,并执行startup函数,startup函数会执行Module.runMain方法,而Module.runMain方法会执行Module._load方法,参数就是命令行的第一个参数(比如: node ./app.js),如此,跟上面require就走到一起了。
要想深入的理解一件事情的原理,还是需要仔细的阅读和研究底层的实现代码,好在node关于require实现原理方面的代码还挺简单的,想要深入理解node的同学还是很有必要仔细读一下的。