精华 Node.js高性能编程之路—模块的加载
发布于 4 年前 作者 classfellow 5384 次浏览 最后一次编辑是 3 年前 来自 分享

本文系原创,转载请注明出处~ 了解Node模块的加载过程,有助于从宏观把握一个Js服务器程序是如何组织起来的。使用Node的vm模块,可以绕过require,直接运行Js代码

Node.js程序的入口

	// file hello.js
	(function (){
	  console.log('hello world!')
	})()

在命令行下执行node hello.js。Node在启动的时候,根据第二个参数 hello.js加载js文件并运行。Node.js没有C++/Java那样的main函数,但hello.js犹如main函数那样,是服务端程序运行的总入口。

vm模块

(function(){
	console.log('hello world~')
})

将以上代码保存到test.js main.js的内容为

const vm = require('vm');
const fs = require('fs');
const path = require('path');
var prt = path.resolve(__dirname, '.', 'test.js');
function stripBOM(content) {
  if (content.charCodeAt(0) === 0xFEFF) {
    content = content.slice(1);
  }
  return content;
}
var wrapper = stripBOM(fs.readFileSync(prt, 'utf8'));
var compiledWrapper = vm.runInThisContext(wrapper, {
    filename: prt,
    lineOffset: 0,
    displayErrors: true
  });
compiledWrapper(); 

运行这个文件,得到hello world~。使用vm模块就能直接运行js文件。在使用require加载js文件的时候,Node.js内部也是如此读取,编译,运行我们的js文件。

Js文件与模块

用Node.js编写程序,一个js文件对应一个模块,在module.js文件中,其构造函数定义如下

function Module(id, parent) {
	  this.id = id;
	  this.exports = {};
	  this.parent = parent;
	  if (parent && parent.children) {
		parent.children.push(this);
	  }
	  this.filename = null;
	  this.loaded = false;
	  this.children = [];
	}

node加载一个js文件时,会先new 一个 Module

var module = new Module(filename, parent);

然后读入Js代码并对文件进行头尾包装。如此这般之后,Js文件里面的代码变成这个匿名函数内部的语句。

    (function (exports, require, module, __filename, __dirname) {
		//原始文件内容
	});

上述形式的代码实际上是一个函数字面量,说的直白点,就是一个定义匿名函数的表达式。Node使用V8编译并运行上面的代码,也就是对以上表达式求值,其值是一个函数对象。V8将结果返回给Node。

//右侧表达式的值是一个函数对象
var fn = (function(){
}) ;

Node得到返回的函数对象,使用apply方法,指定上下文,传入 module.exports,require方法, module,文件名以及路径作为参数,执行函数。在这一步,开始执行Js文件内部的代码。

  	var args = [this.exports, require, this, filename, dirname];
  	var result = compiledWrapper.apply(this.exports, args);  //compiledWrapper 为函数对象

由源代码可知,Js文件运行的上下文环境是module.exports,因此在文件中,也可以直接使用this导出对象。一旦一个文件加载之后,其对应的模块被缓存,其他文件又require的时候,直接取缓存。

	var cachedModule = Module._cache[filename];
  	if (cachedModule) {
    	return cachedModule.exports;
  	}
	//...	

module.exports 或者 exports 区别

从构造函数Module可以看出,他们指向同一个空对象。因此如下代码完全等效

	module.exports.IA = 'ia';
	exports.IA2 = 'ia';
	exports.IAF = function(){
		console.log('');
	}

甚至可以直接使用this导出

this.IA3 = 'ia';

以上代码都不改变exports的值,改变的是其指向的对象。但如果对exports赋值,像这样

	module.exports = {
		print:function(){
			console.log('')
		}
	}

则只能使用module.exports导出,如果exports前不加modulemodule.exports指向的对象仍然是{}

小心使用全局变量

把Js文件包装成一个匿名函数运行,使得Js文件内部定义的变量局限于函数内部,只有用exports导出,外部才可访问。这使得不同文件中,相同的变量名互补干扰。如果定义变量时,前面不加var,则变量成为全局变量。Node的执行环境存在一个全局的对象global,它类似于浏览器里面的window对象。如果代码这样写

	function test(){
		this.xxx = 'xxx';
		yyy = 200;
	}
	test();

就会给global增加两个属性。可以运行

console.log(global);

看一下这个全局对象包含哪些成员。

模块的分类

Node.js 的模块可以分三类 1.V8 自身支持的对象,例如DataMath,这些是语言标准定义的对象,由V8直接提供 2.Node.js作为Js运行时,提供了丰富的API, 实现这些API的c++函数和Js代码,在Node初始化时被加载,这部分模块为NativeModule。c++扩展Js接口的部分过于底层。对于Js代码部分,加载使用NativeModule.require,编译运行完缓存在NativeModule._cache 。相关的代码在bootstrap_node.js中。bootstrap_node.js是Node.js最先执行的一段js代码,文件头部有这样一段注释

	//Hello, and welcome to hacking node.js!
	//This file is invoked by node::LoadEnvironment in src/node.cc, and is
	//responsible for bootstrapping the node.js core. As special caution is given
	//to the performance of the startup process, many dependencies are invoked
	//lazily.	

这个文件中包含如下一段代码

		var Module = NativeModule.require('module');
		//...
   		preloadModules();
 		run(Module.runMain);

可见,主模块在这里被加载执行,Module.runMain在module.js中

	// bootstrap main module.
	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();
	};

3.用户的js文件以及npm安装的模块,根据上面的讨论,这些js文件模块对应Module构造出的对象。require函数由Module.prototype.require定义,加载之后模块缓存在Module._cache

前一篇—内存控制与Stream 后一篇—LoopBack 开源框架

回到顶部