node源码详解(三)—— js代码在node中的位置,process、require、module、exports的由来

5822b4ba3a48ad366440e4f2 Luzeshu 星期六, 2016年3月12日 14

本作品采用知识共享署名 4.0 国际许可协议进行许可。转载联系作者并保留声明头部与原文链接https://luzeshu.com/blog/nodesource3
本博客同步在https://cnodejs.org/topic/56e3dfde545c5c736d12383f
本博客同步在http://www.cnblogs.com/papertree/p/5225994.html


我们用惯了process.nextTick()、require('xxx')、module.exports,但是它们哪里来呢?下文给出答案...

3.1 node main函数到执行js文件的位置

3.1.1 入口

上篇博客2.1中提到src目录存放的是node的C++源码部分,包括main函数。

src/node_main.cc 和 src/node.cc(注:.cc是linux下的C++文件,类似windows下的.cpp)这两个文件就是node的入口文件。

其中node_main.cc里面仅仅作为一个入口,调用node.cc 文件中的node::Start()。

3-1-1.png

图3-1-1

3.1.2 node::Start()到加载js文件

有兴趣可以看一下node::Start()函数做些什么(代码截图放上来了),我们关注的只是里面的StartNodeInstance()这一行。

3-1-2.png

图3-1-2

我们来看一下一连串的调用:
Start() -> StartNodeInstance() -> LoadEnviroment() -> ExecuteString()

这四个函数都在node.cc文件里面,看到LoadEnviroment() 几行关键代码:

3-1-3.png

图3-1-3

最终在LoadEnvrioment()里面加载node.js文件,调用ExecuteString()。
并且在ExecuteString()调用V8的 Script::Compile() 和 Script::Run()两个接口去解析执行js代码。


3.1.3 node.js文件和用户的app.js文件

通过命令行“node app.js”启动,我们希望node执行的是app.js 文件,为什么LoadEnvironment()里面加载的是node.js文件呢?

上篇博客2.1 讲到node的lib文件夹存放原生js模块,而src文件夹里面全部是.cc文件(node C++源码) ,但src下面有一个node.js文件。这个文件的作用是什么呢?



3.2 node.js文件,包装app.js文件,require/module.exports

先来看node.js文件的结构:

3-2-1.png

图 3-2-1

3.2.1 node.js 返回的是一个函数

看到3.1.2 中,在LoadEnvironment()里面调用ExecuteString() 解析执行node.js文件,返回值是一个f_value。
而这个f_value 通过V8的接口 Local::Cast转换成一个Local类型的变量f ,而Local类型\*是V8中表示一个js 函数的C++类型。
在LoadEnvironment()的最后一行通过 f->Call(),去执行node.js返回来的一个函数。
而从node.js文件中也可以看出确实返回的是一个匿名函数。
所以,我们的app.js并不在ExecuteString()里面执行,而是在f->Call()的时候被执行。


3.2.2 LoadEnvironment()之f->Call()与 node.js之startup()

上面说到f->Call()的时候,是在V8里面解析执行了node.js返回来的匿名函数,来看一下该函数(图3-2-1)。 步骤1. 匿名函数的第一个步骤是给startup()函数挂载一堆初始化函数,比如processNext()。
步骤2. startup的相关初始化函数都挂载完之后,匿名函数执行最后一步,调用startup()。
步骤3. 看到startup()函数的定义,里面开始去执行这一堆挂载函数。processNext()里面所做的就是给process.nextTick赋值。就是我们平时用的process.nextTick了。
  注:匿名函数的参数process是C++模块传进来的对象(也就是f->Call()传进来的参数)。
步骤4. startup()函数在执行完匿名函数挂载的一堆初始化函数之后,继而执行Module.runMain()。


3.2.3 Module.runMain()包装app.js文件

看到startup()函数在执行runMain()之前,对process.argv[1]里做了一下处理,这个argv就是node命令行启动的时候,main函数接收的参数。比如“node app.js”启动,argv[1]就保存着我们的js文件名了。

那么runMain()通过process.argv[1]去读取app.js文件,读成字符串,调用NativeModule.wrap()函数(图3-2-1),把我们的app.js代码包在里面。

这里就可以说明app.js代码里面为何生来就有require、exports、module这几个变量可以用了。其实是被放到一个 js函数里面了。

最后runMain()里面也是调用V8的Compile()、Run()等接口,去执行app.js代码。

14 Comments:
e60510bc-f5f2-4e6e-998e-458e9d36c2c3

2016-03-14 07:49:34

hi,楼主有意向来阿里巴巴工作吗,这里绝对是Node.js练兵的好场地~

84065d5a-de73-41b9-915d-ee637d5922d4

2016-03-20 10:06:12

编辑了markdown版本方便这边看~

e9684e29-8559-4316-a01b-a8451f98f04c

2016-03-22 08:27:43

大的一神…我勒个去


2016-03-23 00:10:42

@fangker 不敢当呀

4a29ca97-0794-4bec-993b-92cc2e0b6ab7

2016-03-25 09:42:17

楼主、大爱呀。


2016-03-25 20:06:26

bd90a9c5-45da-451e-b521-9dda57c567fe

2016-03-29 09:55:40

支持大神

2447876a-2b4d-4859-987c-b5fef0cbd7e1

2016-06-22 18:48:26

require、exports、module从哪里还是没有说清楚


2016-06-25 13:06:28

@依然是等待
再往里展开又有一堆东西了,从runMain()开始,就会找到require这些怎么来的,在lib/module.js里面,你可以看看。

263eb935-1d14-4f6f-849a-8c0556199254

2016-08-16 13:58:22

这里有个疑问 NativeModule.require和lib/module.js里面提供的Module.prototype.require方法好像重复了 所以很奇怪,那么在

NativeModule.wrapper = [
	'(function (exports, require, module, __filename, __dirname) { ',
	'\n});'
];

里面的exports, require,module三个值为什么对应的又是Module.prototype.require呢,没找到在哪里赋值的


2016-08-21 00:00:30

@hyj1991 两个地方的require不一样的,我们的js代码里面require是lib/module.js里面的Module.prototype.require,那里面调的Module._load(),里面有个nonInternalExists判断,调的才是NativeModule.require


2016-08-21 00:02:02

@hyj1991 我们普通js文件调的require,一般在Module._load()里面,判断cachedModule,有cache直接返回,没有cache到下面的new Module

31cf9d5d-35c7-42b5-ab2d-6fa6e6ac413e

2016-08-21 13:27:47

@bigtree9307 我后来仔细看了下node.js和module.js,已经明白了,还是非常感谢~

573113f3-f863-4cc8-8f09-f07fc8c428e7

2017-05-19 15:24:05

虽然和最新版本的代码细节不一样,但是博文思路很有帮助。

声明:电子邮箱用于接收回复的通知邮件(来自<mail@luzeshu.com>,请勿回复至此邮箱)。
163邮箱可正常接收,gmail邮箱注意查看垃圾邮件,qq邮箱由于ip频率限制可能无法成功投递。