其实在工作过程中一直有个想法,就是好好地深入的去学习一下 Node.js,无奈各种工作,家庭,生活和一些其他的原因导致此事一直搁浅,之前也有零零散散的学过,但是都算不得上深入,框架的使用也都只是停留在会用的阶段,底层的实现也没有知根知底的去探个究竟
所以打算在这 2019 年剩下的一段时间里,静下心来好好地学一下 Node.js,补充一下相关知识和一些流行框架的内容,应该会是一个系列文章,记录的就是在学习 Node.js 过程当中的一些笔记,心得和想法吧,刚好也看到了 如何正确的学习 Node.js 这篇文章,就以这个为起点,从头开始吧
什么是 Node.js
按照官方的说法是
1 | Node.js® is a JavaScript runtime built on Chrome's V8 JavaScript engine. |
简单的总结一下,主要有下面这几点
Node.js不是语言或者框架,也不是JavaScript的应用,它只是一个JavaScript运行时环境- 它构建在
Chrome's V8的JavaScript引擎之上(Chrome V8引擎以C/C++为主,相当于使用JavaScript写法,转成C/C++调用) - 特点是事件驱动(
event-driven),非阻塞I/O模型(non-blocking I/O model)
用自己的话来说就是
Node.js 不是一门语言也不是框架,它是基于 Google V8 引擎的 JavaScript 运行时环境,同时结合 Libuv 扩展了 JavaScript 功能,使之支持 io、fs 等只有语言才有的特性,使得 JavaScript 能够同时具有 DOM 操作和 I/O、文件读写、操作数据库等能力,一般主要用来开发低延迟的网络应用,也就是那些需要在服务器端环境和前端实时收集和交换数据的应用(如 API、即时聊天、微服务)等
基本原理
如下图,简要的介绍了 Node.js 是基于 Chrome V8 引擎构建的,由事件循环(EventLoop)分发 I/O 任务,最终工作线程(Work Thread)将任务丢到线程池(Thread Pool)里去执行,而事件循环只要等待执行结果就可以了

核心概念
主要分为三个部分
Chrome V8引擎EventLoop事件循环Thread Pool线程池
简单的梳理一下
Chrome V8是JavaScript引擎,而Node.js又内置Chrome V8引擎,所以它使用的JavaScript语法JavaScript语言的一大特点就是单线程,也就是说,同一个时间只能做一件事,这就意味着,所有任务需要排队,如果前一个任务结束,才会执行后一个任务,如果前一个任务耗时很长,后一个任务就不得不一直等着- 由
EventLoop将I/O任务放到线程池里
换一个维度来看,如下图

同样的,我们也来简单的梳理一下
Chrome V8解释并执行JavaScript代码(这就是为什么浏览器能执行JavaScript原因)- 由事件循环和线程池组成,负责所有
I/O任务的分发与执行
在解决并发问题上,异步是最好的解决方案,可以简单的理解为排队和叫号的机制,排队的时候,等待就可以了,而取号的过程,则是由 EventLoop 来接受处理,而真正执行操作的是具体的线程池里的 I/O 任务,之所以说 Node.js 是单线程,就是因为在接受任务的时候是单线程的,它无需在进程或者线程当中切换上下文,所以非常高效,但它在执行具体任务的时候是多线程的
关于更多的应用场景的介绍可以参考 Node.js 应用场景
异步流程控制
Node.js 的核心就是异步流程控制,如下图是 Node.js 解决异步流程问题的演进

- 红色代表
Promise,是使用最多的,无论async还是generator都可用 - 蓝色是
Generator,过度期当中使用的 - 绿色是
Async函数,也是接下来的趋势
所以推荐使用 Async 函数加 Promise 组合

简单来说,就是以下三点
callbackPromiseAsync/Await
下面一个一个来看
Callback && EventEmitter
在这里我们主要看两个点,即 Callback 和 EventEmitter,先来看看 Callback,在 Node.js 当中推崇回调函数使用 Error-first 的写法,也就是错误优先的回调写法,它有两条规则
- 回调函数的第一个参数返回的
error对象,如果error发生了,它会作为第一个err参数返回,如果没有,一般做法是返回null - 回调函数的第二个参数返回的是任何成功响应的结果数据,如果结果正常,没有
error发生,err会被设置为null,并在第二个参数就出返回成功结果数据
1 | function(err, res) { |
关于 EventEmitter,在 Node.js 当中使用的是事件驱动模型,当 webserver 接收到请求,就把它关闭然后进行处理,再去服务下一个 web 请求,当这个请求完成,它被放回处理队列,当到达队列开头,这个结果被返回给用户
这个模型非常高效可扩展性非常强,因为 webserver 一直接受请求而不等待任何读写操作(也就是所谓的事件驱动 IO),在事件驱动模型中,会生成一个主循环来监听事件,当检测到事件时触发回调函数,也就是下图这样

事件模块是 Node.js 内置的对发布订阅模式(publish/subscribe)的实现,通过 EventEmitter 属性,提供了一个构造函数,该构造函数的实例具有 on 方法,可以用来监听指定事件,并触发回调函数,任意对象都可以发布指定事件,被 EventEmitter 实例的 on 方法监听到,下面是一个简单的示例
1 | const EventEmitter = require('events') |
如果同时绑定了多个事件监听器,则事件监听器回调函数是会被先后调用,而事件参数则作为回调函数参数传递,本质上就是发布订阅模式的实现,下面是一个简单的发布订阅模式的手动实现
1 | class Target { |
Promise
Promise 意味着一个还没有完成的操作,但在未来会完成的,Promise 最主要的交互方法是通过将函数传入它的 then 方法从而获取得 Promise 最终的值,要点有三个
- 递归,每个异步操作返回的都是
Promise对象 - 状态机,三种状态转换,只在
Promise对象内部可以控制,外部不能改变状态 - 全局异常处理
定义如下
1 | var promise = new Promise(function (resolve, reject) { |
每个 Promise 定义都是一样的,在构造函数里传入一个匿名函数,参数是 resolve 和 reject,分别代表成功和失败时候的处理,如下
1 | promise.then(function (text) { |
它的主要交互方式是通过 then 函数,如果 Promise 成功执行 resolve 了,那么它就会将 resolve 的值传给最近的 then 函数,作为它的 then 函数的参数,如果出错 reject,那就交给 catch 来捕获异常,更多内容可以参考下面几个链接
终极解决方案 Async/Await
API 的介绍就不详细展开了,我们下面就看两个实际的用法,第一个是一段 Koa 2 应用里的一段代码
1 | exports.list = async (ctx, next) => { |
它做了三件事
- 通过
await Student.getAllAsync()来获取所有的students信息 - 通过
await ctx.render渲染页面 - 由于是同步代码,使用
try/catch做的异常处理
第二个是一个读取文件的操作,采用 await + promise 的写法
1 | const Promise = require('bluebird') |