跳到主要内容

基础

不要混淆nodejs和浏览器中的event loop stop-using-default-exports-javascript-module

按模块进行分层, 对 web 请求,业务逻辑,数据存取操作分层 使用原生错误对象,而不是字符串 区分运行时错误和编码错误 集中进行错误处理,但不要在中间件中处理

模块机制

node 里的模块是什么

Node中,每个文件模块都是一个对象,它的定义如下:

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

module.exports = Module;

var module = new Module(filename, parent);

所有的模块都是 Module 的实例。可以看到,当前模块(module.js)也是 Module 的一个实例。

require 模块加载机制

1、先计算模块路径 2、如果模块在缓存里面,取出缓存 3、加载模块 4、的输出模块的exports属性即可

// require 其实内部调用 Module._load 方法
Module._load = function(request, parent, isMain) {
// 计算绝对路径
var filename = Module._resolveFilename(request, parent);

// 第一步:如果有缓存,取出缓存
var cachedModule = Module._cache[filename];
if (cachedModule) {
return cachedModule.exports;

// 第二步:是否为内置模块
if (NativeModule.exists(filename)) {
return NativeModule.require(filename);
}

/********************************这里注意了**************************/
// 第三步:生成模块实例,存入缓存
// 这里的Module就是我们上面的1.1定义的Module
var module = new Module(filename, parent);
Module._cache[filename] = module;

/********************************这里注意了**************************/
// 第四步:加载模块
// 下面的module.load实际上是Module原型上有一个方法叫Module.prototype.load
try {
module.load(filename);
hadException = false;
} finally {
if (hadException) {
delete Module._cache[filename];
}
}

// 第五步:输出模块的exports属性
return module.exports;
};

加载模块时,为什么每个模块都有__dirname,__filename属性,这两个属性哪里来

// 上面(1.2部分)的第四步module.load(filename)
// 这一步,module模块相当于被包装了,包装形式如下
// 加载js模块,相当于下面的代码(加载node模块和json模块逻辑不一样)
(function (exports, require, module, __filename, __dirname) {
// 模块源码
// 假如模块代码如下
var math = require('math');
exports.area = function(radius){
return Math.PI * radius * radius
}
});

也就是说,每个module里面都会传入__filename, __dirname参数,这两个参数并不是module本身就有的,是外界传入的

node 导出模块有两种方式,exports.xxx=xxx 和 Module.exports={} 有什么区别

  • exports 其实就是 module.exports
module.exports vs exports
很多时候,你会看到,在Node环境中,有两种方法可以在一个模块中输出变量:

方法一:对module.exports赋值:

// hello.js

function hello() {
console.log('Hello, world!');
}

function greet(name) {
console.log('Hello, ' + name + '!');
}

module.exports = {
hello: hello,
greet: greet
};
方法二:直接使用exports:

// hello.js

function hello() {
console.log('Hello, world!');
}

function greet(name) {
console.log('Hello, ' + name + '!');
}

function hello() {
console.log('Hello, world!');
}

exports.hello = hello;
exports.greet = greet;
但是你不可以直接对exports赋值:

// 代码可以执行,但是模块并没有输出任何变量:
exports = {
hello: hello,
greet: greet
};
如果你对上面的写法感到十分困惑,不要着急,我们来分析Node的加载机制:

首先,Node会把整个待加载的hello.js文件放入一个包装函数load中执行。在执行这个load()函数前,Node准备好了module变量:

var module = {
id: 'hello',
exports: {}
};
load()函数最终返回module.exports:

var load = function (exports, module) {
// hello.js的文件内容
...
// load函数返回:
return module.exports;
};

var exportes = load(module.exports, module);
也就是说,默认情况下,Node准备的exports变量和module.exports变量实际上是同一个变量,并且初始化为空对象{},于是,我们可以写:

exports.foo = function () { return 'foo'; };
exports.bar = function () { return 'bar'; };
也可以写:

module.exports.foo = function () { return 'foo'; };
module.exports.bar = function () { return 'bar'; };
换句话说,Node默认给你准备了一个空对象{},这样你可以直接往里面加东西。

但是,如果我们要输出的是一个函数或数组,那么,只能给module.exports赋值:

module.exports = function () { return 'foo'; };
给exports赋值是无效的,因为赋值后,module.exports仍然是空对象{}。

结论
如果要输出一个键值对象{},可以利用exports这个已存在的空对象{},并继续在上面添加新的键值;

如果要输出一个函数或数组,必须直接对module.exports对象赋值。

所以我们可以得出结论:直接对module.exports赋值,可以应对任何情况:

module.exports = {
foo: function () { return 'foo'; }
};
或者:

module.exports = function () { return 'foo'; };
最终,我们强烈建议使用module.exports = xxx的方式来输出模块变量,这样,你只需要记忆一种方法。

异步 IO

事件循环流程

  • 在进程启动时,Node便会创建一个类似于while(true)的循环,每执行一次循环体的过程我们成为Tick。

  • 每个Tick的过程就是查看是否有事件待处理。如果有就取出事件及其相关的回调函数。然后进入下一个循环,如果不再有事件处理,就退出进程。

在每个tick的过程中,如何判断是否有事件需要处理呢?

  • 每个事件循环中有一个或者多个观察者,而判断是否有事件需要处理的过程就是向这些观察者询问是否有要处理的事件。

  • 在Node中,事件主要来源于网络请求、文件的I/O等,这些事件对应的观察者有文件I/O观察者,网络I/O的观察者。

  • 事件循环是一个典型的生产者/消费者模型。异步I/O,网络请求等则是事件的生产者,源源不断为Node提供不同类型的事件,这些事件被传递到对应的观察者那里,事件循环则从观察者那里取出事件并处理。

  • 在windows下,这个循环基于IOCP创建,在*nix下则基于多线程创建

描述一下异步 IO 流程

参考