其实在工作过程中一直有个想法,就是好好地深入的去学习一下 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
组合
简单来说,就是以下三点
callback
Promise
Async/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') |