# 基础概念

Node.js 是一个使用单线程, 事件驱动、非阻塞式 (异步式) I/O 的模型, 基于 Chrome V8 引擎JavaScript 运行环境。允许 Js 可以脱离浏览器去执行.

要实现在后台运行 JavaScript 代码,代码需要先被解释然后正确的执行。Node.js 的原理正是如此,它使用了 Google 的 V8 虚拟机( Google 的 Chrome 浏览器使用的 JavaScript 执行环境,来解释和执行 JavaScript 代码。

# 单线程, 非阻塞 I/O 和 事件驱动

Node.js 最大的特点就是应用 非阻塞 I/O 与 事件驱动的编程模式。

# 什么是阻塞?

在说非阻塞之前, 先了解什么是阻塞. 线程在执行中如果遇到磁盘读写或网络通信(统称为 I/O 操作), 通常要耗费较长的时间. 这时候操作系统会剥夺这个线程的 CPU 控制权,使其暂停执行,同时将资源让给其他的工作线程,这种线程调度方式称为阻塞。 当 I/O 操作完毕时,操作系统 将这个线程的阻塞状态解除,恢复其对CPU的控制权,令其继续执行。这种 I/O 模式就是通常的 阻塞式 I/O

相应地,异步式 I/O 则针对 所有 I/O 操作不采用阻塞的策略。当线程遇到 I/O 操作时,不会以阻塞的方式等待 I/O 操作 的完成或数据的返回. 而只是将 I/O 请求发送给操作系统,继续执行下一条语句。 当操作 系统完成 I/O 操作时,以事件的形式通知执行 I/O 操作的线程,线程会在特定时候处理这个 事件。为了处理异步 I/O,线程必须有事件循环,不断地检查有没有未处理的事件,依次予以处理。这种 I/O 模式就是通常的 非阻塞式 I/O

# 非阻塞好处

因为采用了非阻塞, 个线程永远在执行计算操作,这个线程所使用的 CPU 核心利用率几近是 100%,可以提高服务器的利用率, 提高系统吞吐量.

假设我们有一项工作,可以分为两个计算部分和一个 I/O 部分,I/O 部分占的时间比计算多得多。如果我们使用阻塞 I/O,那么要想获得高并发就必须开启多个线程。而使用异步式 I/O 时,单线程即可胜任。

Screen Shot 2018-07-12 at 4.37.15 PM

Screen Shot 2018-07-12 at 4.37.22 PM

总而言之, 使用单线程事件驱动的异步式 I/O 可以避免服务器对于多线程的开销, 提高对服务器CPU性能的利用效率. 不好的地方, 可以就在于理解起来不直观吧.

Screen Shot 2018-07-12 at 4.41.05 PM

# 回调函数

在 Node.js 中,异步式 I/O 是通过回调函数来实现的。

先看看代码的不同:

// 同步式读取文件
var fs = require('fs');
var data = fs.readFileSync('file.txt', 'utf-8');
console.log(data);
// 异步式读取文件
var fs = require('fs');
fs.readFile('file.txt', 'utf-8', function(err, data) {
    if(err) {
        console.log(err);
    } else {
        console.log(data);
    }
})

同步式读取代码将文件名作为参数传入 fs.readFileSync 函 数,阻塞等待读取完成后,将文件的内容作为函数的返回值赋给 data 变量

异步式读取文件是通过回调函数来实现的. fs.readFile 接收了三个参数, 第一个是文件名,第二个是编码方式,第三个是一个函数,我们称这个函数为回调函数。 fs.readFile 调用时所做的工作只是将异步式 I/O 请求发送给了操作系统,然后立即 返回并执行后面的语句,执行完以后进入事件循环监听事件。当 fs 接收到 I/O 请求完成的事件时,事件循环会主动调用回调函数以完成后续工作。

# 事件

Node.js 所有的异步 I/O 操作在完成时都会发送一个事件到事件队列。事件由 EventEmitter 对象提供。

EventEmitter 使用例子:

// 引入 events 模块
var events = require('events');

// 创建 eventEmitter 对象
var event = new events.EventEmitter();

// 注册事件 some_event
event.on('some_event', function() { 
    console.log('some_event occured.');
});

setTimeout(function() { 
    // 触发事件 some_event
    event.emit('some_event');
}, 1000);

代码运行一秒后, 控制台输出 'some_event occured'.

运行机制是 event 对象 注册了事件 some_event 的一个监听器, 通过 setTimeout 在1000毫秒以后向 event 对象发送事件 some_event,此时会调用绑定在 some_event监听器上的事件处理函数。

# 事件循环机制

Node.js 在什么时候会进入事件循环呢? 答案是 Node.js 始终在事件循环中.

Node.js 程序由事件循环开始,到事件循环结束,所有的逻辑都是事件的回调函数. 程序入口就是事件循环第一个事件的回调函数。事件的回调函数在执行的过程中,可能会发出 I/O 请求或 直接发射(emit)事件,执行完毕后再返回事件循环,事件循环会检查事件队列中有没有未处理的事件,直到程序结束。

Node.js的事件循环对开发者是不可见, 所有的一切均被 EventEmitter 封装

Screen Shot 2018-07-12 at 5.07.35 PM

上次更新: 7/4/2020, 4:14:54 AM