什么是 Node.js

什么是 Node.js

其实在工作过程中一直有个想法,就是好好地深入的去学习一下 Node.js,无奈各种工作,家庭,生活和一些其他的原因导致此事一直搁浅,之前也有零零散散的学过,但是都算不得上深入,框架的使用也都只是停留在会用的阶段,底层的实现也没有知根知底的去探个究竟

所以打算在这 2019 年剩下的一段时间里,静下心来好好地学一下 Node.js,补充一下相关知识和一些流行框架的内容,应该会是一个系列文章,记录的就是在学习 Node.js 过程当中的一些笔记,心得和想法吧,刚好也看到了 如何正确的学习 Node.js 这篇文章,就以这个为起点,从头开始吧

什么是 Node.js

按照官方的说法是

1
2
3
4
5
Node.js® is a JavaScript runtime built on Chrome's V8 JavaScript engine. 

Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient.

Node.js' package ecosystem, npm, is the largest ecosystem of open source libraries in the world.

简单的总结一下,主要有下面这几点

  • Node.js 不是语言或者框架,也不是 JavaScript 的应用,它只是一个 JavaScript 运行时环境
  • 它构建在 Chrome's V8JavaScript 引擎之上(Chrome V8 引擎以 C/C++ 为主,相当于使用JavaScript 写法,转成 C/C++ 调用)
  • 特点是事件驱动(event-driven),非阻塞 I/O 模型(non-blocking I/O model

用自己的话来说就是

Node.js 不是一门语言也不是框架,它是基于 Google V8 引擎的 JavaScript 运行时环境,同时结合 Libuv 扩展了 JavaScript 功能,使之支持 iofs 等只有语言才有的特性,使得 JavaScript 能够同时具有 DOM 操作和 I/O、文件读写、操作数据库等能力,一般主要用来开发低延迟的网络应用,也就是那些需要在服务器端环境和前端实时收集和交换数据的应用(如 API、即时聊天、微服务)等

基本原理

如下图,简要的介绍了 Node.js 是基于 Chrome V8 引擎构建的,由事件循环(EventLoop)分发 I/O 任务,最终工作线程(Work Thread)将任务丢到线程池(Thread Pool)里去执行,而事件循环只要等待执行结果就可以了

核心概念

主要分为三个部分

  • Chrome V8 引擎
  • EventLoop 事件循环
  • Thread Pool 线程池

简单的梳理一下

  • Chrome V8JavaScript 引擎,而 Node.js 又内置 Chrome V8 引擎,所以它使用的 JavaScript 语法
  • JavaScript 语言的一大特点就是单线程,也就是说,同一个时间只能做一件事,这就意味着,所有任务需要排队,如果前一个任务结束,才会执行后一个任务,如果前一个任务耗时很长,后一个任务就不得不一直等着
  • EventLoopI/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

在这里我们主要看两个点,即 CallbackEventEmitter,先来看看 Callback,在 Node.js 当中推崇回调函数使用 Error-first 的写法,也就是错误优先的回调写法,它有两条规则

  • 回调函数的第一个参数返回的 error 对象,如果 error 发生了,它会作为第一个 err 参数返回,如果没有,一般做法是返回 null
  • 回调函数的第二个参数返回的是任何成功响应的结果数据,如果结果正常,没有 error 发生,err 会被设置为 null,并在第二个参数就出返回成功结果数据
1
2
3
function(err, res) {
// process the error and result
}

关于 EventEmitter,在 Node.js 当中使用的是事件驱动模型,当 webserver 接收到请求,就把它关闭然后进行处理,再去服务下一个 web 请求,当这个请求完成,它被放回处理队列,当到达队列开头,这个结果被返回给用户

这个模型非常高效可扩展性非常强,因为 webserver 一直接受请求而不等待任何读写操作(也就是所谓的事件驱动 IO),在事件驱动模型中,会生成一个主循环来监听事件,当检测到事件时触发回调函数,也就是下图这样

事件模块是 Node.js 内置的对发布订阅模式(publish/subscribe)的实现,通过 EventEmitter 属性,提供了一个构造函数,该构造函数的实例具有 on 方法,可以用来监听指定事件,并触发回调函数,任意对象都可以发布指定事件,被 EventEmitter 实例的 on 方法监听到,下面是一个简单的示例

1
2
3
4
5
6
7
8
9
10
11
const EventEmitter = require('events')

class MyEmitter extends EventEmitter {}

const myEmitter = new MyEmitter()

myEmitter.on('event', () => {
console.log('触发了一个事件')
})

myEmitter.emit('event')

如果同时绑定了多个事件监听器,则事件监听器回调函数是会被先后调用,而事件参数则作为回调函数参数传递,本质上就是发布订阅模式的实现,下面是一个简单的发布订阅模式的手动实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class Target {
constructor() {
this.list = {}
}

// 订阅
lister(type, fn) {
if (this.list[type]) {
this.list[type].push(fn)
}
this.list[type] = [fn]
}

// 发布
trigger(type, ...args) {
this.list[type].forEach(list => {
list(...args)
})
}

// 删除
remove(type, fn) {
let fns = this.list[type]
if (!fns) return false
if (fn) {
for (let i = 0; i < fns.length; i++) {
let _fn = fns[i]
if (_fn === fn) {
fns.splice(i, 1)
}
}
}
}
}

// 使用
const target = new Target()

const clickHandle = function(e) { console.log(`click, ${e}`) }
const dbClickHandle = function(e) { console.log(`dbClick, ${e}`) }

target.lister('click', clickHandle)
target.lister('dbclick', dbClickHandle)
target.remove('click', clickHandle)

target.trigger('click', 'zhangsan')
target.trigger('click', 'lisi')
target.trigger('dbclick', 'wangwu')

Promise

Promise 意味着一个还没有完成的操作,但在未来会完成的,Promise 最主要的交互方法是通过将函数传入它的 then 方法从而获取得 Promise 最终的值,要点有三个

  • 递归,每个异步操作返回的都是 Promise 对象
  • 状态机,三种状态转换,只在 Promise 对象内部可以控制,外部不能改变状态
  • 全局异常处理

定义如下

1
2
3
4
5
6
7
var promise = new Promise(function (resolve, reject) {
if (/* everything turned out fine */) {
resolve('Stuff worked!')
} else {
reject(Error('It broke'))
}
})

每个 Promise 定义都是一样的,在构造函数里传入一个匿名函数,参数是 resolvereject,分别代表成功和失败时候的处理,如下

1
2
3
4
5
6
promise.then(function (text) {
console.log(text)
return `Promise`.reject(new Error('Error'))
}).catch(function (err) {
console.log(err)
})

它的主要交互方式是通过 then 函数,如果 Promise 成功执行 resolve 了,那么它就会将 resolve 的值传给最近的 then 函数,作为它的 then 函数的参数,如果出错 reject,那就交给 catch 来捕获异常,更多内容可以参考下面几个链接

终极解决方案 Async/Await

API 的介绍就不详细展开了,我们下面就看两个实际的用法,第一个是一段 Koa 2 应用里的一段代码

1
2
3
4
5
6
7
8
9
10
exports.list = async (ctx, next) => {
try {
let students = await Student.getAllAsync()
await ctx.render('students/index', {
students: students
})
} catch (err) {
return ctx.api_error(err)
}
}

它做了三件事

  • 通过 await Student.getAllAsync() 来获取所有的 students 信息
  • 通过 await ctx.render 渲染页面
  • 由于是同步代码,使用 try/catch 做的异常处理

第二个是一个读取文件的操作,采用 await + promise 的写法

1
2
3
4
5
6
7
8
9
const Promise = require('bluebird')
const fs = Promise.promisifyAll(require('fs'))

async function test() {
const contents = await fs.readFileAsync('myfile.js', 'utf8')
console.log(contents)
}

test()

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×