Node核心:异步流程控制
Node.js是为异步而生的,它自己把复杂的事儿做了(高并发,低延时),交给用户的只是有点难用的Callback写法。也正是坦诚的将异步回调暴露出来,才有更好的流程控制方面的演进。也正是这些演进,让Node.js从DIRT(数据密集实时应用)扩展到更多的应用场景,今天的Node.js已经不只是能写后端的JavaScript,已经涵盖了所有涉及到开发的各个方面,而Node全栈更是热门种的热门。
直面问题才能有更好的解决方式,Node.js的异步是整个学习Node.js过程中重中之重。
- 1) 异步流程控制学习重点
- 2)Api写法:Error-first Callback 和 EventEmitter
- 3)中流砥柱:Promise
- 4)终极解决方案:Async/Await
1) 异步流程控制学习重点
我整理了一张图,更直观一些。从09年到现在,8年多的时间里,整个Node.js社区做了大量尝试,其中曲折足足够写一本书的了。大家先简单了解一下。
- 红色代表Promise,是使用最多的,无论async还是generator都可用
- 蓝色是Generator,过度货
- 绿色是Async函数,趋势
结论:Promise是必须会的,那你为什么不顺势而为呢?
推荐:使用Async函数 + Promise组合,如下图所示。
其实,一般使用是不需要掌握上图中的所有技术的。对于初学者来说,先够用,再去深究细节。所以,精简一下,只了解3个就足够足够用了。
结论
- Node.js SDK里callback写法必须会的。
- Node.js学习重点: Async函数与Promise
- 中流砥柱:Promise
- 终极解决方案:Async/Await
所以下面我们会分个小部分进行讲解。
2)Api写法:Error-first Callback 和 EventEmitter
a)Error-first Callback
定义错误优先的回调写法只需要注意2条规则即可:
- 回调函数的第一个参数返回的error对象,如果error发生了,它会作为第一个err参数返回,如果没有,一般做法是返回null。
- 回调函数的第二个参数返回的是任何成功响应的结果数据。如果结果正常,没有error发生,err会被设置为null,并在第二个参数就出返回成功结果数据。
下面让我们看一下调用函数示例,Node.js 文档里最常采用下面这样的回调方式:
function(err, res) {
// process the error and result
}
这里的 callback
指的是带有2个参数的函数:”err”和 “res”。语义上讲,非空的“err”相当于程序异常;而空的“err”相当于可以正常返回结果“res”,无任何异常。
b)EventEmitter
事件模块是 Node.js 内置的对观察者模式“发布/订阅”(publish/subscribe)的实现,通过EventEmitter
属性,提供了一个构造函数。该构造函数的实例具有 on
方法,可以用来监听指定事件,并触发回调函数。任意对象都可以发布指定事件,被 EventEmitter
实例的 on
方法监听到。
在node 6之后,可以直接使用require('events')
类
var EventEmitter = require('events')
var util = require('util')
var MyEmitter = function () {
}
util.inherits(MyEmitter, EventEmitter)
const myEmitter = new MyEmitter();
myEmitter.on('event', (a, b) => {
console.log(a, b, this);
// Prints: a b {}
});
myEmitter.emit('event', 'a', 'b');
和jquery、vue里的Event是非常类似的。而且前端自己也有EventEmitter。
c)如何更好的查Node.js文档
API是应用程序接口Application Programming Interface的简称。从Node.js异步原理,我们可以知道,核心在于 Node.js SDK 中API调用,然后交由EventLoop(Libuv)去执行,所以我们一定要熟悉Node.js的API操作。
Node.js的API都是异步的,同步的函数是奢求,要查API文档,在高并发场景下慎用。
笔者推荐使用 Dash 或 Zeal 查看离线文档,经常查看离线文档,对Api理解会深入很多,比IDE辅助要好,可以有效避免离开IDE就不会写代码的窘境。
3)中流砥柱:Promise
回调地狱
Node.js 因为采用了错误优先的回调风格写法,导致sdk里导出都是回调函数。如果组合调用的话,就会特别痛苦,经常会出现回调里嵌套回调的问题,大家都非常厌烦这种写法,称之为Callback Hell,即回调地狱。一个经典的例子来自著名的Promise模块q文档里。
step1(function (value1) {
step2(value1, function(value2) {
step3(value2, function(value3) {
step4(value3, function(value4) {
// Do something with value4
});
});
});
});
这里只是做4步,嵌套了4层回调,如果更多步骤呢?很多新手浅尝辄止,到这儿就望而却步,粉转黑。这明显不够成熟,最起码你要看看它的应对解决方案吧!
Node.js 约定所有Api都采用错误优先的回调方式,这部分场景都是大家直接调用接口,无太多变化。而Promise是对回调地狱的思考,或者说是改良方案。目前使用非常普遍,可以说是在async函数普及之前唯一一个通用性规范,甚至 Node.js 社区都在考虑 Promise 化,可见其影响之大。
Promise最早也是在commonjs社区提出来的,当时提出了很多规范。比较接受的是promise/A规范。后来人们在这个基础上,提出了promise/A+规范,也就是实际上现在的业内推行的规范。ES6 也是采用的这种规范。
Promise意味着[许愿|承诺]一个还没有完成的操作,但在未来会完成的。与Promise最主要的交互方法是通过将函数传入它的then方法从而获取得Promise最终的值或Promise最终最拒绝(reject)的原因。要点有三个:
- 递归,每个异步操作返回的都是promise对象
- 状态机:三种状态转换,只在promise对象内部可以控制,外部不能改变状态
- 全局异常处理
1)定义
var promise = new Promise(function(resolve, reject) {
// do a thing, possibly async, then…
if (/* everything turned out fine */) {
resolve("Stuff worked!");
}
else {
reject(Error("It broke"));
}
});
每个Promise定义都是一样的,在构造函数里传入一个匿名函数,参数是resolve和reject,分别代表成功和失败时候的处理。
2)调用
promise.then(function(text){
console.log(text)// Stuff worked!
return Promise.reject(new Error('我是故意的'))
}).catch(function(err){
console.log(err)
})
它的主要交互方式是通过then函数,如果Promise成功执行resolve了,那么它就会将resolve的值传给最近的then函数,作为它的then函数的参数。如果出错reject,那就交给catch来捕获异常就好了。
Promise 的最大优势是标准化,各类异步工具库都按照统一规范实现,即使是async函数也可以无缝集成。所以用 Promise 封装 API 通用性强,用起来简单,学习成本低。在async函数普及之前,绝大部分应用都是采用Promise来做异步流程控制的,所以掌握Promise是Node.js学习过程中必须要掌握的重中之重。
Bluebird是 Node.js 世界里性能最好的Promise/a+规范的实现模块,Api非常齐全,功能强大,是原生Promise外的不二选择。
好处如下:
- 避免Node.js内置Promise实现 问题,使用与所有版本兼容
- 避免Node.js 4曾经出现的内存泄露问题
- 内置更多扩展,timeout、 promisifyAll等,对Promise/A+规范提供了强有力的补充
限于时间关系,这里就不一一列举了,还是那句话,在学习Node.js过程中,对于Promise了解多深入都不过分。
推荐学习资料
- Node.js最新技术栈之Promise篇 https://cnodejs.org/topic/560dbc826a1ed28204a1e7de
- 理解 Promise 的工作原理 https://cnodejs.org/topic/569c8226adf526da2aeb23fd
- Promise 迷你书 http://liubin.github.io/promises-book/
4)终极解决方案:Async/Await
Async/Await是异步操作的终极解决方案,Koa 2在node 7.6发布之后,立马发布了正式版本,并且推荐使用async函数来编写Koa中间件。
这里给出一段Koa 2应用里的一段代码
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);
}
};
它做了3件事儿
- 通过await Student.getAllAsync();来获取所有的students信息。
- 通过await ctx.render渲染页面
- 由于是同步代码,使用try/catch做的异常处理
是不是非常简单,现在Eggjs里也都是这样同步的代码。
4.1 正常写法
const pkgConf = require('pkg-conf');
async function main(){
const config = await pkgConf('unicorn');
console.log(config.rainbow);
//=> true
}
main();
变态写法
const pkgConf = require('pkg-conf');
(async () => {
const config = await pkgConf('unicorn');
console.log(config.rainbow);
//=> true
})();
4.2 await + Promise
const Promise = require('bluebird');
const fs = Promise.promisifyAll(require("fs"));
async function main(){
const contents = await fs.readFileAsync("myfile.js", "utf8")
console.log(contents);
}
main();
4.3 await + co + generator
const co = require('co');
const Promise = require('bluebird');
const fs = Promise.promisifyAll(require("fs"));
async function main(){
const contents = co(function* () {
var result = yield fs.readFileAsync("myfile.js", "utf8")
return result;
})
console.log(contents);
}
main();
要点
- co的返回值是promise,所以await可以直接接co。
- co的参数是genrator
- 在generator里可以使用yield,而yield后面接的有5种可能,故而把这些可以yield接的方式称为yieldable,即可以yield接的。
- Promises
- Thunks (functions)
- array (parallel execution)
- objects (parallel execution)
- Generators 和 GeneratorFunctions
由上面3中基本用法可以推出Async函数要点如下:
- Async函数语义上非常好
- Async不需要执行器,它本身具备执行能力,不像Generator需要co模块
- Async函数的异常处理采用try/catch和Promise的错误处理,非常强大
- Await接Promise,Promise自身就足够应对所有流程了,包括async函数没有纯并行处理机制,也可以采用Promise里的all和race来补齐
- Await释放Promise的组合能力,外加co和Promise的then,几乎没有不支持的场景
综上所述
- Async函数是趋势,如果Chrome 52. v8 5.1已经支持Async函数 ( https://github.com/nodejs/CTC/issues/7 )了,Node.js支持还会远么?
- Async和Generator函数里都支持promise,所以promise是必须会的。
- Generator和yield异常强大,不过不会成为主流,所以学会基本用法和promise就好了,没必要所有的都必须会。
- co作为Generator执行器是不错的,它更好的是当做Promise 包装器,通过Generator支持yieldable,最后返回Promise,是不是有点无耻?
小结
这部分共讲了4个小点,都是极其直接的必须掌握的知识点。
- 1) 异步流程控制学习重点
- 2)Api写法:Error-first Callback 和 EventEmitter
- 3)中流砥柱:Promise
- 4)终极解决方案:Async/Await
这里再提一下关于Node.js源码阅读问题,很多人api都还没玩儿熟练就去阅读源码,这是非常不赞成的,不带着问题去读源码是比较容易迷失在大量代码中的。效果并不好。
先用明白,然后再去阅读Node.js源码,然后探寻libuv并发机制。很多人买了朴大的《深入浅出Node.js》一书,看了之后还是不太会用,不是书写的不好,而是步骤不对。
- Node in action和了不起的Node.js是入门的绝好书籍,非常简单,各个部分都讲了,但不深入,看了之后,基本就能用起来了
- 当你用了一段之后,你会对Node.js的运行机制好奇,为啥呢?这时候去读朴大的《深入浅出Node.js》一书就能够解惑。原因很简单,九浅一深一书是偏向底层实现原理的书,从操作系统,并发原理,node源码层层解读。如果是新手读,难免会比较郁闷。
- 实践类的可以看看雷宗民(老雷)和赵坤(nswbmw)写的书
我一般给大家的推荐是把Node in action读上5遍10遍,入门干活足够了。剩下的就是反复实践,多写代码和npm模块就好。
目前所有的书籍几乎都有点过时了,大部分都是Node.js v0.10左右的版本的,我的新书是基于Node.js 8版本的,预计2018年3月或4月出版。别催我,真没法更快了。
目录
- [01 Node.js初识]
- [02 安装与入门]
- [03 更了不起的Node.js]
- [04 更好的Node.js]
- [05 Node.js是如何执行的]
- [06 模块与核心]
- [07 异步写法与流程控制]
- [08 下一代Web框架Koa入门]
- [09 Koa的核心扩展机制:中间件]
- [10 HTTP协议必知必会]
- [11 Koa练习]
- [12 数据库入门]
- [13 数据库进阶]
- [14 视图模板]
- [15 Koa项目实战]
- [16 自己动手写NPM模块]
- [17 Node.js企业级Web开发]
- [18 构建具有Node.js特色的微服务]
- [19 让Node.js跑的更稳]
- [20 让Node.js跑的更快]
博文视点的美女编辑在苦逼的整理中,预计出版在3月之后,20章,800页+,定价预计在130+。