# 为什么用Node.js

  • 简单强大,轻量可扩展
  • javascript,json来进行编码,web开发必备技能
  • 非阻塞IO,可以适应分块传输数据,较慢的网络环境,尤其擅长高并发访问
  • 前后端使用统一语言
  • 可扩展体现在可以轻松应对多实例,多服务器架构,同时有海量的第三方应用组件

# 事件循环和非阻塞IO

# 单线程

  • 传统web服务中,大多都是使用多线程机制来解决并发的问题,原因是I/O事件会阻塞线程,而阻塞就意味着要等待
  • node的设计是采用了单线程的机制,只是针对 主线程来说,即每个node进程只有一个主线程来执行程序代码
  • 采用了事件驱动的机制,将耗时阻塞的I/O操作交给线程池中的某个线程去完成
  • 主线程本身只负责不断地调度,并没有执行真正的I/O操作。也就是说node实现的是异步非阻塞式。

底层,Node.js借助libuv来作为抽象封装层,从而屏蔽不同操作系统的差异

libuv: linux下用libev实现,Windows下用IOCP实现

IO多路复用模型

# 事件循环机制

根据node的官方介绍,node每次事件循环机制都包含了6个阶段:

  • timers阶段:这个阶段执行已经到期的timer(setTimeout、setInterval)回调
  • I/O callbacks阶段:执行I/O(例如文件、网络)的回调
  • idle, prepare 阶段:node内部使用
  • poll阶段:获取新的I/O事件, 适当的条件下node将阻塞在这里
  • check阶段:执行setImmediate回调
  • close callbacks阶段:执行close事件回调,比如TCP断开连接

对于日常开发来说,我们比较关注的是timers、I/O callbacks、check阶段

  • node和浏览器相比一个明显的不同就是node在每个阶段结束后会去执行所有 process.nextTick 、microtask(promise)任务

事件循环原理

  • node 的初始化
    • 初始化 node 环境。
    • 执行输入代码。
    • 执行 process.nextTick 回调。
    • 执行 microtasks。
  • 进入 event-loop
    • 进入 timers 阶段
      • 检查 timer 队列是否有到期的 timer 回调,如果有,将到期的 timer 回调按照 timerId 升序执行。
      • 检查是否有 process.nextTick 任务,如果有,全部执行。
      • 检查是否有microtask,如果有,全部执行。
      • 退出该阶段。
    • 进入IO callbacks阶段
      • 检查是否有 pending 的 I/O 回调。如果有,执行回调。如果没有,退出该阶段
      • 检查是否有 process.nextTick 任务,如果有,全部执行
      • 检查是否有microtask,如果有,全部执行
      • 退出该阶段
    • 进入 idle,prepare 阶段:
      • 这两个阶段与我们编程关系不大,暂且按下不表。
    • 进入 poll 阶段
    • 进入 check 阶段
    • 进入 closing 阶段
    • 检查是否有活跃的 handles(定时器、IO等事件句柄)
      • 如果有,继续下一轮循环。
      • 如果没有,结束事件循环,退出程序。

可以发现,在事件循环的每一个子阶段退出之前都会按顺序执行如下过程:

  • 检查是否有 process.nextTick 回调,如果有,全部执行
  • 检查是否有 microtasks,如果有,全部执行
  • 退出当前阶段

同一次事件循环中,微任务永远在宏任务之前执行

# 异步事件

非I/O:

  • 定时器(setTimeout,setInterval)
  • process.nextTick
  • microtask(promise)
  • setImmediate
  • DNS.lookup

I/O:

  • 网络I/O
  • 文件I/O
  • 一些DNS操作

# 宏任务和微任务

任务队列又分为macro-task(宏任务)micro-task(微任务),在最新标准中,它们被分别称为taskjobs

当前执行栈执行完毕时会立刻先处理所有微任务队列中的事件,然后再去宏任务队列中取出一个事件。同一次事件循环中,微任务永远在宏任务之前执行

setTimeout(_ => console.log(4))

new Promise(resolve => {
  resolve()
  console.log(1)
}).then(_ => {
  console.log(3)
})

console.log(2)
1
2
3
4
5
6
7
8
9
10

输出: 1,2,3,4

  1. new Promise => 同步执行
  2. Promise.then() => Promise.then()中注册的回调才是异步执行的;具有代表性的微任务
  3. setTimeout就是作为宏任务来存在的
  • 微任务:process.nextTick、Promise.then catch finally
  • 宏任务: I/O,setTimeout、setInterval、setImmediate,script(整体代码),UI rendering

# node的构架

主要分为三层: 应用app >> V8及node内置架构 >> 操作系统

V8是node运行的环境,可以理解为node虚拟机.

node内置架构又可分为三层: 核心模块(javascript实现) >> c++绑定 >> libuv + CAes + http

node的构架

# node核心内置库及实现原理

常用的核心内置库如事件EventEmitter流Stream文件fs网络net,http,https进程管理process、cluster

# global

全局对象包括:模块变量函数,Buffer类,Timer函数,process, console

# 模块

  1. __dirname
  2. __filename
  3. exports
  4. module
  5. require()

# 定时器Timer

js中有哪些定时器?

setTimeout/clearTimeout, setInterval/clearInterval, setImmediate/clearImmediate

任务执行顺序: 同步 > process.nextTick() > Promise > setTimeout(,less time) > setImmediate > setTimeout(,more time)

// test.js
setTimeout(() => console.log(1)); // 4. 而setTimeout、setInterval、setImmediate的回调函数,追加在次轮循环。
setImmediate(() => console.log(2)); // 5. 而setTimeout、setInterval、setImmediate的回调函数,追加在次轮循环。
process.nextTick(() => console.log(3)); // 2. process.nextTick和Promise的回调函数,追加在本轮循环,即同步任务一旦执行完成,就开始执行它们。
Promise.resolve().then(() => console.log(4));// 3. process.nextTick和Promise的回调函数,追加在本轮循环,即同步任务一旦执行完成,就开始执行它们。
(() => console.log(5))();   // 1. 同步任务总是比异步任务更早执行。
1
2
3
4
5
6

运行结果如下

5 
3
4
1
2
1
2
3
4
5

setTimeout和setImmediate,如果setTimeout的时间足够小,则setTimeout先执行,如果为什么?

  1. 查看 Timer源码,setImmediate 和 setTimeout最终都是在一个 时间堆PriorityQueue 上进行执行
  2. 但是,一开始setImmediates先加入队列immediateQueue使用双端链表linkedlist实现
  3. 执行的时候,setImmediates从队列进入 时间堆PriorityQueue,这个过程是有代价的,所有如果 setTimeout是0ms或者说时间足够小,setTimeout先执行

# 进程管理

child_process, cluster

# child_process创建子进程的方式

  • .exec()、.execFile()、.fork()底层都是通过.spawn()实现的。
  • .exec()、execFile()额外提供了回调,当子进程停止的时候执行

风险项

传入的命令,如果是用户输入的,有可能产生类似sql注入的风险,比如

exec('ls hello.txt', function(error, stdout, stderr){}) 恶意攻击 => exec('ls hello.txt; rm -rf *', function(error, stdout, stderr){})

# cluster

master 进程通过 process.fork() 创建子进程,他们之间通过 IPC (内部进程通信)通道实现通信。

操作系统的进程间通信方式: 共享内存,消息传递(socket,rpc),信号量,管道,消息队列等

  1. 共享内存;不同进程共享同一段内存空间。通常还需要引入信号量机制,来实现同步与互斥。
  2. 消息传递;这种模式下,进程间通过发送、接收消息来实现信息的同步。nodejs父子进程通过事件机制通信,就是这种模型
  3. 信号量;信号量简单说就是系统赋予进程的一个状态值,未得到控制权的进程会在特定地方被强迫停下来,等待可以继续进行的信号到来。如果信号量只有 0 或者 1 两个值的话,又被称作“互斥锁”。这个机制也被广泛用于各种编程模式中。
  4. 管道;用于连接两个进程,将一个进程的输出作为另一个进程的输入

Node.js 为父子进程的通信提供了事件机制EventEmmiter来传递消息。

# 负载均衡策略

Node.js 默认采用的策略是 round-robin 时间片轮转法。

负载均衡算法:

  • round-robin 时间片轮转法; 每一次把来自用户的请求轮流分配给各个进程,不足:处理效率不一样,会出现负载不均衡
  • WRR (weight-round-robin) 加权轮转法

时间片轮转法(round-robin)不适用于windows, 第二种方式是由主进程创建 socket 监听端口后, 将 socket 句柄直接分发给相应的 worker, 然后当连接进来时, 就直接由相应的 worker 来接收连接并处理;存在负载不均衡的问题, 比如通常 70% 的连接仅被 8 个进程中的 2 个处理, 而其他进程比较清闲.

# 多进程的端口监听
  1. master 进程负责监听端口
  2. 然后将连接通过某种分发策略(比如 round-robin),转发给 worker 进程。
  3. 这样由于只有 master 进程接收客户端连接,就解决了竞争导致的负载不均衡的问题
  4. 关键:要求 master 进程的稳定性足够好

# EventEmitter

EventEmitter是node中一个实现观察者模式的类,主要功能是监听和发射消息,用于处理多模块交互问题

# 代码实现

var util = require('util');
var EventEmitter = require('events').EventEmitter;

function MyEmitter() {
    EventEmitter.call(this);
} // 构造函数

util.inherits(MyEmitter, EventEmitter); // 继承

var em = new MyEmitter();
em.on('hello', function (data) {
    console.log('收到事件hello的数据:', data);
}); // 接收事件,并打印到控制台
em.emit('hello', 'EventEmitter传递消息真方便!');
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# EventEmitter有哪些典型应用

  1. 模块间传递消息
  2. 回调函数内外传递消息
  3. 处理流数据,因为流是在EventEmitter基础上实现的.
  4. 观察者模式发射触发机制相关应用

# 怎么捕获EventEmitter的错误事件

监听error事件即可.如果有多个EventEmitter,也可以用domain来统一处理错误事件

var domain = require('domain');
var myDomain = domain.create();
myDomain.on('error', function (err) {
    console.log('domain接收到的错误事件:', err);
}); // 接收事件并打印
myDomain.run(function () {
    var emitter1 = new MyEmitter();
    emitter1.emit('error', '错误事件来自emitter1');
    emitter2 = new MyEmitter();
    emitter2.emit('error', '错误事件来自emitter2');
});
1
2
3
4
5
6
7
8
9
10
11

# EventEmitter中的newListenser事件有什么用处

newListener可以用来做事件机制的反射,特殊应用,事件管理等

当任何on事件添加到EventEmitter时,就会触发newListener事件,基于这种模式,我们可以做很多自定义处理.

var emitter3 = new MyEmitter();
emitter3.on('newListener', function(name, listener) {
	console.log("新事件的名字:", name);
	console.log("新事件的代码:", listener);
	setTimeout(function(){ console.log("我是自定义延时处理机制"); }, 1000);
});
emitter3.on('hello', function(){
	console.log('hello node');
});
1
2
3
4
5
6
7
8
9

# Stream

stream是基于事件EventEmitter的数据管理模式.由各种不同的抽象接口组成,主要包括可写,可读,可读写,可转换等几种类型

# Stream有什么好处

非阻塞式数据处理提升效率,片断处理节省内存,管道处理方便可扩展等

# Stream有哪些典型应用

文件,网络,数据转换,音频视频等

# 怎么捕获Stream的错误事件

监听error事件,方法同EventEmitter

# 有哪些常用Stream,分别什么时候使用

  • 可读流Readable,在作为输入数据源时使用;
  • 可写流Writable,在作为输出源时使用;
  • 双工流Duplex,它作为输出源接受被写入,同时又作为输入源被后面的流读出.
  • 转换流Transform,跟Duplex一样,都是双向流,但它的输出与输入是相关联的
    • 需要实现一个函数_transfrom(chunk, encoding, callback);而Duplex需要分别实现_read(size)函数和_write(chunk, encoding, callback)函数

# 缓冲

highWaterMark

可缓冲的数据大小取决于传入流构造函数的 highWaterMark 选项。 对于普通的流, highWaterMark 指定了字节的总数。 对于对象模式的流, highWaterMark 指定了对象的总数。

限制数据的缓冲到可接受的程度,也就是读写速度不一致的源头与目的地不会压垮内存

# 实现一个Writable Stream

三步走:

  1. 继承Writable
  2. 覆写原型链方法_write(chunk, encoding, callback)函数

代码实现:

var Writable = require('stream').Writable;
var util = require('util');

function MyWritable(options) {
	Writable.call(this, options); // 构造继承
} 
util.inherits(MyWritable, Writable); // 继承Writable
MyWritable.prototype._write = function(chunk, encoding, callback) {
	console.log("被写入的数据是:", chunk.toString()); // 此处可对写入的数据进行处理
	callback();
};

process.stdin.pipe(new MyWritable()); // stdin作为输入源,MyWritable作为输出源   
1
2
3
4
5
6
7
8
9
10
11
12
13

# 文件系统fs

Node通过fs模块来和文件系统进行交互,该模块提供了一些标准的文件访问API类打开、读取、写入文件、以及与其交互。

# 内置的fs模块架构是什么样子的

fs模块主要由下面几部分组成:

  1. POSIX文件操作的封装,对应于操作系统的原生文件操作; unlink,stat,rename...
  2. 文件流 fs.createReadStream和fs.createWriteStream
  3. 同步文件读写, fs.readFileSync和fs.writeFileSync
  4. 异步文件读写, fs.readFile和fs.writeFile

# 读写一个文件有多少种方法

总体来说有四种:

  1. POSIX式底层读写
  2. 流式读写
  3. 同步读写
  4. 异步读写

# 怎么读取json配置文件

  1. 利用node内置的require('data.json')机制 (注意:其中一个改变了js对象,其它跟着改变)
  2. 读入文件入内容,然后用JSON.parse(content)转换成js对象

两种方式的区别:

  1. require机制情况下,如果多个模块都加载了同一个json文件,那么其中一个改变了js对象,其它跟着改变,这是由node模块的缓存机制造成的,只有一个js模块对象
  2. 第二种方式则可以随意改变加载后的js变量,而且各模块互不影响,因为他们都是独立的,是多个js对象

# fs.watch和fs.watchFile有什么区别

二者主要用来监听文件变动

  1. fs.watch利用操作系统原生机制来监听,可能不适用网络文件系统;
  2. fs.watchFile则是定期检查文件状态变更,适用于网络文件系统,但是相比fs.watch有些慢,因为不是实时机制.

# 网络

# node的网络模块架构是什么样子的

node全面支持各种网络服务器和客户端,包括http/https, tcp, udp, dns, tls/ssl

# node是怎样支持https,tls的

要实现以下几个步骤即可:

  1. openssl生成公钥私钥
  2. 服务器或客户端使用https替代http
  3. 服务器或客户端加载公钥私钥证书

# 优雅退出

graceful模块配合cluster就可以实现这个解决方案。

graceful是基于domain模块实现的

domain;能捕捉异步回调中出现的异常。(弥补try...catch...的不足)

process.on('uncaughtException)

# 错误优先的回调函数

错误优先的回调函数用于传递错误和数据。第一个参数始终应该是一个错误对象, 用于检查程序是否发生了错误。其余的参数用于传递数据。

只有遵循错误优先回调的函数可以promisify

# 如何避免回调地狱

  • 使用三方库,Q, blubird, async进行promisify
  • 使用Promise链式调用
  • 使用yield+生成器generator或Promise, co
  • 使用async, await语法糖ES6

# 什么是stub

TDD、Stub和Mock

stub存在的意图是为了让测试对象可以正常的执行,硬编码一些输入和输出

mock除了保证stub的功能之外,还可深入的模拟对象之间的交互方式,如:调用了几次、在某种情况下是否会抛出异常

stub是 mock 的子集

# express的路由机制

express是如何从一个中间件执行到下一个中间件的?

A: app.use()

app.use()的原理是什么?

function middleware(req,res,next){
    // 做该干的事

    // 做完后调用下一个函数
    next(); // next也是一个函数,它表示函数数组中的下一个函数
}
1
2
3
4
5
6

其实所有中间件函数,是顺序添加到中间件数组里面,这个函数数组表示在发出响应之前要执行的所有函数

使用app.use(fn)后,传进来的fn就会被扔到这个数组里,执行完毕后调用next()方法执行函数数组里的下一个函数,如果没有调用next()的话,就不会调用下一个函数了,也就是说调用就会被终止

# Express和Koa的区别

  1. Handler 处理方式
    • Express 使用普通的回调函数,一种线性的逻辑,在同一个线程上完成所有的 HTTP 请求;异步操作的执行顺序不确定;回调的方式不利于错误捕获;
    • 使用ES7的Async/Await替换了原来的 Generator + co 的模式; Async/Await 现在也称为 JS 异步的终极解决方案。
  2. 中间件实现机制
    • koa2: 中间件 Compose;洋葱圈模型;会去等待异步(Promise)完成;可以非常方便的实现后置处理逻辑
    • Express 中间件实现是基于 Callback 回调函数同步的,它不会去等待异步(Promise)完成
  3. 响应机制
    • 在 Koa 中数据的响应是通过 ctx.body 进行设置,注意这里仅是设置并没有立即响应,而是在所有的中间件结束之后做了响应;这样做一个好处是我们在响应之前是有一些预留操作空间的,
    • express:res.send() 之后就立即响应了,这样如果还想在上层中间件做一些操作是有点难的。

其实,Express 也是类似的洋葱模型,不同的是:

Express 中间件机制使用了 Callback 实现,这样如果出现异步则可能会使你在执行顺序上感到困惑,因此如果我们想做接口耗时统计、错误处理 Koa 的这种中间件模式处理起来更方便些。最后一点响应机制也很重要,Koa 不是立即响应,是整个中间件处理完成在最外层进行了响应,而 Express 则是立即响应。

# egg.js和nest.js

  • egg.js是在koa的基础上做了一层很好的面向大型企业级应用的框架封装
  • egg支持ts
  • egg.js更多的是按照洋葱模型的开发方式,和AOP编程还是有点区别的
  • NEST.js配合typeorm可以在node下拥有不输Spring的面向切面编程的体验