Node.js
一、异步 I/O
1. 异步I/O的概念
当一个异步过程调用出发后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态,通知和回调来通知调用者。
接下来先来回顾一下Ajax异步请求:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 1/* javascript Ajax 请求 */
2var xhr = new XMLHttpRequest();
3xhr.open('get','xxx.php',true);
4xhr.send();
5xhr.onreadystatechange = funtion(){
6 console.log('收到响应');
7}
8console.log('发送Ajax请求结束');
9
10/* jQuery Ajax 请求 */
11$.post('xxx.php',function(data){
12 console.log('收到响应');
13})
14console.log('发送Ajax请求结束');
15
16
熟悉异步请求的用户都知道,'收到响应’是在’发送Ajax请求结束’之后输出的。在发送请求后,后续代码时会被立即执行的,而’收到响应’的执行时间是不被预期的。我们只知道它将会在这个异步请求结束后执行,但并不知道具体的时间,只能通过事件的监听、回调函数获取响应数据。异步调用中对结果值得捕获完全符合"Don't call me,I will call you"都原则,这也是只注重结果,不注重过程的表现。
经典的Ajax异步调用。
在Node中,异步I/O也是非常常见的,以阅读文件为例,我们可以观察到他与前端Ajax调用的方式及其类似。
1
2
3
4
5
6
7 1var fs = require('fs');
2fs.readFile('./xxx.html',function(err,data){
3 console.log('读取文件完成');
4})
5console.log('发送读取文件');
6
7
同样"读取文件完成"的输出会晚于"发送读取文件",同样"读取文件完成"的执行也取决于读取文件的异步调用何时结束。
经典的异步调用。
在Node中,绝大多数的操作都是以异步的方式进行调用的。
同步I/O与异步I/O,阻塞I/O与非阻塞I/O的区别:
- 同步I/O:同步就是在一个功能调用时,在没有得到结果之前,该调用就不返回。也就是一件一件事做,等前一件做完了才做下一件事。
- 异步I/O:异步和同步相对,当一个异步过程调用出发后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态,通知和回调来通知调用者。
- 阻塞I/O:就是在IO执行的两个阶段(等待数据 和拷贝数据两个阶段)数据没来,啥都不做,直到数据来了,才进行下一步的处理,CPU会等待I/O,造成等待资源浪费。
- 非阻塞I/O:实现了同时服务多个客户端,能够在等待任务完成的时间里干其他活了,包括提交其他任务。但是不停的轮询recv,占用较多的cpu资源,造成资源的浪费。
扩展:轮询
由于完整的I/O并没有完成,立即返回的并不是业务层期望的数据,而是仅仅表示当前调用的状态。为了获取完整的数据,应用程序需要重复调用I/O操作来确认是否完成。这种重复调用判断操作是否完成的技术叫做轮询。
https://blog.csdn.net/Errrl/article/details/103979471
2. 为什么需要异步I/O
为什么node要设计异步I/O,这与node面向网络而设计不无关系。
(1)用户体验
在《高性能JavaScript》一书中曾经总结过,如果脚本的执行时间超过100ms,用户就会感觉到页面的卡顿,以为页面停止了响应。而这其中的关键在于网络速度的限制给网页的实时体验造成很大的麻烦。如果网页临时需要获取一个网络资源,通过同步的方式获取,那么JavaScript则需要等待资源完全从服务器端获取后才能继续执行,这期间的UI将会停顿,不响应用户的交互行为。可想而知,这样的用户体验将会有多差。
可以这样理解,假如一个资源来自两个不同位置的数据返回,第一个资源需要消耗M毫秒,第二个资源需要消耗N毫秒,如果使用同步的方式总耗时就是M+N毫秒,而如果使用的是异步的方式则是 max(M,N)。
可以这么说只有后端能够快速响应资源,才能然前端的体验变好
(2)资源分配
接下来就从资源分配的角度分析node为什么要设置异步I/O。
假设业务场景中有一组互不相关的任务需要完成,现行阶段主流的方法有以下两种:
- 单线程串行顺序执行
单线程顺序执行任务的方式比较符合编程人员按顺序思考的思维方式。它依然是最主流的编程方式,因为它易于表达。但是顺序执行的缺点在于性能,任意一个略忙的任务还未执行完毕就会导致后续执行代码被阻塞。在计算机资源中,通常I/O与CPU计算之间是并行进行的。但是同步的编程模块导致的问题是,I/O的进行会让后续任务等待,这造成资源不能被更好地利用。
- 多线程并行完成
相比于单线程串行顺序执行,多线程并行完成的代价在于创建线程和执行期线程上下文切换的开销比较大,同时还会面临多线程死锁、状态同步等等的问题。
因此node在两者之间做出了自己的方案:利用单线程,远离多线程死锁、状态同步等问题;利用异步I/O,让单线程远离阻塞,以更好地利用CPU资源。
3. Node 的异步I/O实现
Node中完成整个异步I/O环节有事件循环、观察者和请求对象。
(1)事件循环
首先,着重强调一下Node自身的执行模型——事件循环,正是它使得回调函数十分普遍。
在进程启动时,Node会创建一个while循环,每执行一次循环体的过程我们称为Tick,每个Tick的过程就是查看是否有事件待处理,如果有,就取出事件以及相关的回调函数,如果存在关联的回调函数就执行,然后进入下一个循环,如果不再有事件处理,就退出进程。
(2)观察者
在每一个Tick的过程中,如何判断是否还有事件需要处理?
在这里必须引入的概念就是观察者。——IOCP相关的GetQueuedCompletionStatus()方法。
每一个事件循环中都存在一个或多个观察者,而判断是否有事件要处理的过程就是向这些观察者询问是否又要处理的事件。
(3)请求对象
对于一般的(非异步)回调函数,函数由我们自行调用,比如:
1
2
3
4
5
6
7
8
9
10
11
12 1var list = [1,2,3,4];
2function callback(a,b,c){
3 console.log(a,b,c);
4}
5var forEach = function(list,callback){
6 for(var i = 0;i<list.length;i++){
7 callback(list[i],i,list);
8 }
9}
10forEach(list,callback);
11
12
对于Node中的异步I/O调用而言,回调函数却不由开发者来调用。那么从我们发出调用后,到回调函数被执行,中间发生了什么?事实上,从JavaScript发起调用到内核执行完I/O操作的过渡过程中,存在一种中间产物,它就叫做请求对象。
(4)总结:完整的异步I/O流程
其中的IOCP就与I/O观察者相关。
事件循环、观察者、请求对象、I/O线程池四者共同构成了Node异步I/O模型的基本要素。
二、异步编程
有异步I/O,就必须有异步编程。异步的广泛使用使得回调、嵌套的出现。
1. 异步编程的优势与难点
(1)优势
只要合理地利用Node异步模块与V8的高性能,就可以充分发挥CPU和I/O资源的优势。
(2)难点
Node异步编程借助于异步I/O模型以及V8高性能引擎,突破单线程的性能瓶颈,让JavaScript在后端达到了实用的价值。另一方面它统一了前后端JavaScript的编程模型。接下来就来梳理一下异步编程中的难点,以更好地使用Node。
- 难点一:异常处理
在使用Node前通常使用try/catch/finally语句块进行异常的捕获。
但是对于异步编程而言并不一定适用。所以Node在处理异常上形成了一种约定,将异常作为回调函数的第一个实参传回,如果为空值,则表明没有异常抛出:
1
2
3
4
5 1async function(err,res){
2 //do something...
3}
4
5
在我们自行编写的异步方法上,如果有对异常捕获的需求,也需遵循这样的一些规则:
1)必须执行调用者的回调函数;
2)正确传回异常共调用者判断;
示例如下:
1
2
3
4
5
6
7
8
9
10
11 1var async = function(callback){
2 process.nextTick(function(){
3 var res = /* something */;
4 if(error){
5 return callback(error);
6 }
7 callback(null,res);
8 })
9}
10
11
值得注意的是:在异步编程中容易犯的错误是对用户传递的回调函数进行了异常的捕获:
1
2
3
4
5
6
7
8
9
10
11 1/* 错误 */
2try{
3 req.body = JSON.parse(buf,option.reviver);
4 callback();
5}catch(err){
6 err.body = buf;
7 err.status = 400;
8 callback(err);
9}
10
11
上述示例中会发现一个问题,就是异常捕获的目标发生了改变,原本是想捕获JSON.parse中可能出现的问题,反而现在却是捕获回调函数的异常,同时还发现回调函数不经意间被执行了两次。因此这种情况极有可能造成业务的混乱,正确的做法是:
1
2
3
4
5
6
7
8
9
10
11 1/* 正确 */
2try{
3 req.body = JSON.parse(buf,option.reviver);
4}catch(err){
5 err.body = buf;
6 err.status = 400;
7 return callback(err);
8}
9callback();
10
11
- 难点二:函数嵌套太深
对于Node而言,事务中存在多少个异步调用的场景比比皆是,就比如遍历目录的操作,代码如下:
1
2
3
4
5
6
7
8
9
10
11 1var fs = require('fs');
2fs.readdir('./',function(err,data){
3 data.forEach(function(keys,value){
4 fs.readFile(value,'utf8',function(err,res){
5 //do something...
6 //...
7 })
8 })
9})
10
11
对于上述的场景,由于两次操作存在依赖,函数的嵌套情有可原。那么在网页渲染过程中,通常需要模板、数据、资源文件,这三者互相之间并不依赖,当最终渲染结果中三者缺一不可。如果采取默认的异步方法调用,程序书写会是如下:
1
2
3
4
5
6
7
8
9 1fs.readFile('模板地址','utf8',function(err,datas){
2 数据库.query('SQL语句',function(err,data){
3 资源模块.get(function(err,resources){
4 //do something...
5 })
6 })
7})
8
9
如果根据上述情况,应为嵌套深度在未来node有可能是最难看的代码,当现实情况还不是那么糟糕,接下来来看看如何解决问题。
- 难点三:阻塞代码
在php中有sleep方法(线程沉睡),但是JavaScript没有,唯独能用于延时的操作就只有setInterval和setTimeout这两个函数。如果了解这两个函数的清楚它们都是异步的,也就是说这两个函数并不能阻塞后续代码的持续执行。满足不了阻塞后置代码执行的需求,如果想要满足需求则:
1
2
3
4
5
6
7 1var start = new Data();
2while(new Data()-start<1000){
3 //等待...
4}
5//后置代码(任务需要阻塞的代码)
6
7
看上去好像与sleep(1000)效果相同,但事实上这段代码比sleep(1000)更加可怕,这段代码会持续占有CPU进行判断,与真正的sleep(1000)线程沉睡相距甚远,完全破坏了事件循环的调度,由于Node单线程的原因,CPU资源全都用在了这段代码中,导致其与任何请求都会得不到响应。
遇到这种情况,还是建议在同一业务逻辑后,调用setTimeout()进行延时,反而效果会更好。
2. 异步编程的解决方案
(1)事件发布/订阅模式
事件监听器模式是一种广泛用于异步编程的模式,是回调函数的事件化,又被称为发布/订阅模式。
Node自身提供events模块是发布/订阅模式一个简单的实现,Node中部分模块都继承自它,这个模块不存在事件的冒泡也不存在prevent Default()、stopPropagation()和stopImmediatePropagation()等控制事件传递的方法。它具有的是addListener/on()、once()、removeListener()、removeAllListener()以及emit()等基本事件监听模式的方法实现。
简单的发布与订阅
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 1const EventEmitter = require('events');
2const myEmitter = new EventEmitter();
3//const events = require('events');
4//const myEmitter = new events.EventEmitter();
5
6// 第一个监听器。订阅一
7myEmitter.on('event', function firstListener() {
8 console.log('第一个监听器');
9});
10// 第二个监听器。订阅二
11myEmitter.on('event', function secondListener(arg1, arg2) {
12 console.log(`第二个监听器中的事件有参数 ${arg1}、${arg2}`);
13});
14// 第三个监听器。订阅三
15myEmitter.on('event', function thirdListener(...args) {
16 const parameters = args.join(', ');
17 console.log(`第三个监听器中的事件有参数 ${parameters}`);
18});
19
20console.log(myEmitter.listeners('event'));
21
22/* 发布 */
23myEmitter.emit('event', 1, 2, 3, 4, 5);
24
25// 输出:
26// [
27// [Function: firstListener],
28// [Function: secondListener],
29// [Function: thirdListener]
30// ]
31//
32// 第一个监听器
33// 第二个监听器中的事件有参数 1、2
34// 第三个监听器中的事件有参数 1, 2, 3, 4, 5
35
36
1.1应用场景
1.1.1 继承events模块
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 1/************* Node中Stream对象继承EventEmitter的例子 *************/
2const util = require('util');
3const EventEmitter = require('events');
4
5function MyStream() {
6 EventEmitter.call(this);
7}
8
9util.inherits(MyStream, EventEmitter);
10
11MyStream.prototype.write = function(data) {
12 this.emit('data', data);
13};
14
15const stream = new MyStream();
16
17console.log(stream instanceof EventEmitter); // true
18console.log(MyStream.super_ === EventEmitter); // true
19
20stream.on('data', (data) => {
21 console.log(`接收的数据:"${data}"`);
22});
23stream.write('运作良好!'); // 接收的数据:"运作良好!"
24
25
26/************* 使用ES6类进行继承 *************/
27const EventEmitter = require('events');
28
29class MyStream extends EventEmitter {
30 write(data) {
31 this.emit('data', data);
32 }
33}
34
35const stream = new MyStream();
36
37stream.on('data', (data) => {
38 console.log(`接收的数据:"${data}"`);
39});
40stream.write('使用 ES6');
41
42
开发者可以通过这两种方式轻松地继承EventEmitter类,利用事件处理机制解决业务。
1.1.2 利用事件队列解决雪崩问题
所谓的雪崩问题就是在高访问量、大并发量的情况下缓存失效的情况,此时大量的请求同时涌入数据库中,数据库无法同时承受如此大的查询请求,进而影响网站整体的响应速度。
以下是一条数据库查询语句的调用:
1
2
3
4
5
6
7 1var select = function(callback){
2 数据库模块.select('SQL语句',function(res){
3 callback(res);
4 });
5};
6
7
如果站点 刚好启动,这时缓存是不存在数据的,如果访问量过大,同一句SQL语句被发送到数据库中反复查询,会影响服务器整体的性能。
解决方案一:
1
2
3
4
5
6
7
8
9
10
11
12
13 1/* 设置状态锁,只执行第一次数据库查询,后续的select()是没有数据服务的。 */
2var status = 'ready';
3var select = function(callback){
4 if(status === 'ready'){
5 status = 'pending';
6 数据库模块.select('SQL语句',function(res){
7 status = 'ready';
8 callback(res);
9 });
10 }
11}
12
13
解决方案二:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 1/* 引入事件队列,利用once()监听事件的方法解决雪崩 */
2var events = require('events');
3var proxy = new events.EventsEmitter();
4var status = 'ready';
5var select = function(callback){
6 /* once()事件监听,订阅 */
7 proxy.once('selected',res)
8 if(status === 'ready'){
9 status = 'pending';
10 数据库模块.select('SQL语句',function(res){
11 status = 'ready';
12 /* 发布 */
13 proxy.emit('selected',res);
14 });
15 }
16}
17
18
这里我们使用once()的方法,将所有请求的回调都压入事件队列中,利用其仅执行一次后监视器移除的特性,保证每一次回调只执行一次。
1.1.3 利用事件发布/订阅解决方嵌套深度问题
上述难点二中的异步函数嵌套深度很有可能造成回调地狱,解决的方法就是发布/订阅,串行执行。
难点二:
1
2
3
4
5
6
7
8
9 1fs.readFile('模板地址','utf8',function(err,datas){
2 数据库.query('SQL语句',function(err,data){
3 资源模块.get(function(err,resources){
4 //do something...
5 })
6 })
7})
8
9
解决难点二
方法一:调用函数多对一收敛
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 1/* 渲染 */
2var count = 0;
3var res = {};
4var done = function(key,value){
5 res[key] = value;
6 count++;
7 if(count===3){
8 //渲染页面
9 render(res);
10 };
11};
12
13/* 串行执行 */
14fs.readFile('模板地址','utf8',function(err,datas){
15 done('datas',datas);
16})
17数据库.query('SQL语句',function(err,data){
18 done('data',data);
19})
20资源模块.get(function(err,resources){
21 done('resources',resources);
22});
23
24
(2)Promise/Deferred模式
2.1 Promise/A
Promise抽象定义:
- 一个promise必须有3个状态,pending未完成态,fulfilled(resolved)完成态,rejected失败态。
- 当处于pending状态的时候,可以转化为fulfilled(resolved)或者rejected状态,不可逆。当处于fulfilled(resolved)状态或者rejected状态的时候,就不能互相转化。
- promise的状态一旦转化就不能更改。
对于then()方法的抽象定义:
- 接受完成态、失败态的回调方法,在操作完成或失败时将会调用对应的方法。
- then()方法只能接受function对象,其余对象将会被忽略。
- then()方法继续返回promise对象,以实现链式调用。
node环境中手写Promise原理
JavaScript:(test.js)
1)让promise继承events每一种状态订阅一个事件;使用then存放所有的回调函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 1/* promise.js */
2/* 继承events */
3const util = require('util');
4const eventEmitter = require('events');
5var Promise = module.exports = function(){
6 eventEmitter.call(this);
7};
8util.inherits(Promise,eventEmitter);
9Promise.prototype.then = (fulfilledHandler,rejectHandler,pedingHandler)=>{
10 if(typeof fulfilledHandler === 'function'){
11 this.once('resolve',fulfilledHandler);
12 }
13 if(typeof rejectHandler === 'function'){
14 this.once('reject',rejectHandler);
15 }
16 if(typeof pedingHandler === 'function'){
17 this.once('peding',pedingHandler);
18 }
19 return this;
20}
21
22
2)定义状态的改变的构造函数,发布事件,而这个定义状态改变的构造函数的实例对象就是Deferred。
PromiseA+与Deferred关系如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 1/* deferred.js */
2/* 定义状态构造函数,发布事件 */
3
4var Promise = require('./promise');
5
6var Deferred = module.exports = function(){
7 this.state = 'unfulfilled';
8 this.promise = new Promise();
9}
10Deferred.prototype.resolve = function(obj){
11 this.state = 'fulfilled';
12 this.promise.emit('resolve',obj);
13}
14Deferred.prototype.reject = function(err){
15 this.state = 'rejected';
16 this.promise.emit('reject',err);
17}
18Deferred.prototype.peding = function(data){
19 this.promise.emit('peding',data);
20}
21
22
3)实现接口
-
作用一:触发事件的发布
-
作用二:作用就是绑定http response的res的data、end、error事件侦听器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 1/* test.js */
2/* 定义接口,触发事件的发布 */
3
4var Deferred = require('./deferred');
5var http = require('http');
6
7var interface = function(res){
8 var deferred = new Deferred();
9 var result = '';
10 res.on('data',chunk=>{//相当于peding
11 result += chunk;
12 deferred.peding(chunk);
13 });
14 res.on('end',()=>{//相当于resolved
15 deferred.resolve(result);
16 });
17 res.on('err',(err)=>{//相当于rejected
18 deferred.reject(err);
19 });
20 /* 使得触发后能够.then */
21 return deferred.promise;
22}
23
24
4)测试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 1/* test.js */
2// 以http调用为例
3var options = {
4 hostname: "www.baidu.com",
5 method: "GET"
6};
7var req = http.request(options, function(res){
8 res.setEncoding('utf8');
9 promisify(res).then(function () {
10 console.log("end");
11 }, function(err) {
12 console.log("error:", err);
13 }, function (chunk) {
14 console.log("Received %s bytes", chunk.length);
15 });
16});
17/* 输出结果 */
18req.end();
19
20
5)执行
拿node中读取文件为例,读取一个文件,经过Promsie/Deferred封装后,会变成如下形式:
修改接口:
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 1/* test.js */
2/* 定义接口,触发事件的发布 */
3var fs = require('fs');
4var Deferred = require('./deferred');
5
6/* 读文件 */
7var interface = function(path){
8 var deferred = new Deferred();
9 fs.readFile(path,'utf8',(err,data)=>{
10 if(err){
11 deferred.reject(err);
12 }
13 deferred.resolve(data);
14 })
15 return deferred.promise;
16}
17/* 写文件 */
18var interface2 = function (path, content) {
19 var deferred = new Deferred();
20 fs.writeFile(path, content, 'utf8', (err, data) => {
21 if (err) {
22 deferred.reject(err);
23 }
24 deferred.resolve(data);
25 })
26 return deferred.promise;
27}
28
29
测试:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 1/* test.js */
2/* 读取文件,写文件 */
3interface("./test.txt")
4.then((data) => {
5 console.log(data);
6}, (err) => {
7 console.log(err);
8})
9.then(
10interface("./test2.txt")
11.then((data) => {
12interface2('./test3.txt', data)
13.then(() => {
14console.log('写入成功');
15}, (err) => {
16console.log(err);
17})
18}, (err) => {
19console.log(err);
20}))
21
22
结果:
2.2 使用q:npm install q
给方法定义一个 promise 规范:修改接口:test.js
Q.defer:
手动封装一个promise
特点:使用deferd对象的reject方法(失败回调)、resolve方法(成功回调)、promise属性来实现自定义promise。(前面2个nfcal、denodeify底层应该也是用deferd实现的)
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 1var fs = require('fs');
2var Q = require('q');
3/* 读文件 */
4var interface = function(file){
5 var defer = Q.defer();
6 fs.readFile(file, "utf8",function(err,data){
7 if(!err){
8 defer.resolve(data);//成功就返回数据
9 }else{
10 defer.reject(err);//失败就返回错误
11 }
12 });
13 return defer.promise;//必须返回promise
14}
15/* 写文件 */
16var interface2 = function (path, content) {
17 var defer = Q.defer();
18 fs.writeFile(path, content, 'utf8', (err, data) => {
19 if(!err){
20 defer.resolve(data);//成功就返回数据
21 }else{
22 defer.reject(err);//失败就返回错误
23 }
24 })
25 return defer.promise;
26}
27
28
Q.all
将一批promise封装成一个promise
特点:可以并行执行一批promise,全部执行完毕后一起返回,得到的结果是一个数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 1Q.all([//执行多个函数
2 interface("./test.txt"),
3 interface("./test2.txt"),
4 interface2('./test3.txt','写入成功')
5])
6.spread(function (rtn1,rtn2,rtn3) {
7 //对应的多个返回值
8 console.log(rtn1,rtn2,rtn3);
9 return rtn1+rtn2+rtn3;
10})
11.done(function(result){
12 console.log(result);
13});
14
15
以及一些常用方法:
- Q.makeNodeResolver:
手动封装一个promise
特点:和第deferd原理差不多,只不过用了deferd自带的方法省掉了我们手动实现reject方法、resolve方法
示例:
1
2
3
4
5
6
7
8
9 1var readfile_d=function(filename){
2 var defer=Q.defer();
3 fs.readFile(filename,"utf8",defer.makeNodeResolver());
4 return defer.promise;
5}
6
7readfile_d(filename).then(...);
8
9
- Q
将数据封装成promise
特点:封装一个数据,调用then直接得到该数据
示例:
1
2
3
4
5
6 1Q('hello world').then(data=>{
2 console.log(data);
3})
4//=> hello world
5
6
- Q.fcall
将同步方法封装成promise
特点:传递一个function,返回一个promise,调用then得到方法的返回值
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 1Q.fcall(function(){
2 return interface('test.txt');
3})
4.then(function(data1){
5 return interface('test2.txt');
6})
7.then(function(data2){
8 return interface2('test3.txt',data2);
9})
10.catch(function (error) {
11
12})
13.done(function(res){
14 console.log(res);
15});
16//=> undefined
17
18
- Q.nfcall:
将异步方法封装成promise
特点:封闭时就得传递调用方法的参数,直接得到promise
示例:
1
2
3 1Q.nfcall(fun,p1,p2).then(...);
2
3
- Q.denodeify:
将异步方法封装成promise
特点:封装后返回一个方法,调用此方法得到promise
示例:
1
2
3
4 1var dd = Q.denodeify(fun);
2dd(p1,p2).then(...);
3
4
(3)流程库控制(Generator/async/await函数)
3.1 Generator函数(function*)与异步
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 1/* 封装一个promise函数 */
2const readFile = path => {
3 const promise = new Promise((resolve, reject) => {
4 fs.readFile(path,(err,data) => {
5 if(err) {
6 reject(err);
7 return;
8 }
9 resolve(data);
10 return;
11 })
12 })
13 return promise;
14}
15/* Generator生成器函数 */
16const readFileGenerator = function* (path) {
17 const data1 = yield readFile(path);
18 const data2 = yield readFile(data1.toString());
19 console.log(data1.toString());
20 console.log(data2.toString());
21}
22
23// 使用
24const test1 = readFileGenerator('./test5.txt');
25let readPromise = test1.next(); //启动Generator
26readPromise.value.then(data => {
27 const readPromise = test1.next(data);
28 readPromise.value.then(data2 => {
29 test1.next(data2)
30 });
31})
32// ./test5.txt文件的内容为:./test6.txt
33// ./test6.txt文件的内容为:我是test6文件
34// 打印结果:
35// './test6.txt'
36// '我是test6文件'
37
38
3.2 async/await与异步
执行流程:
为一个函数声明async关键字,告诉js引擎这个函数是一个async函数,也就是需要进行异步处理的一个函数。
async函数内部可以使用await关键字来暂停代码的执行(类似Generator的yield关键字)
await后面跟随的是一个promise对象,然后async会等待await后面的那个promise状态变为resolve时,代码会继续往下执行,并且其promsie的返回值就是await关键字的返回值。
每碰到一个await,就会暂停一次,直到async函数执行完返回。
注意:
async返回的是一个promise对象,也就是说,你可以在await后面跟随另外一个async函数或者promise函数
async返回的promise对象p1,如果async函数内部,抛出了异常,并且没有捕获,那么异常不会被抛出到async函数之外,而是被p1这个promise对象给捕获,导致p1的状态变为reject。
async的返回值,会被他返回的p1这个promise对象使用,如果是返回原始类型,则用于p1的resolve方法的data,如果是抛出异常,则用于p1的reject方法的data。
await也可以跟随原始类型,那么此时就等同于同步操作。
只要一个await语句后面的 Promise 变为reject,那么整个async函数都会中断执行(我猜测类似于使用了Generator函数的迭代器的throw方法抛出了异常)。如果想继续执行,你可以使用try…catch来包裹代码,进行异常捕获。
如果await后面跟的promise已经是处于resolve或者reject,那么他会根据promise的状态进行直接返回或者抛出异常(应该是下一次nextTick或者是类似setTimeout(() => {}, 0)的执行)。
实例:
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 1const readFile = (path) => {
2 const promise = new Promise((resolve, reject) => {
3 fs.readFile(path, (err, data) => {
4 if (err) {
5 reject(err);
6 return;
7 }
8 resolve(data);
9 });
10 });
11
12 return promise;
13};
14
15
16async function asy() {
17 const res1 = await readFile('./test5.txt');
18 const res2 = await readFile('./test6.txt');
19 console.log(res1);
20 console.log(res2);
21}
22
23asy();
24// 打印结果:
25// './test6.txt'
26// '我是test6文件'
27
28
上述例子其实还不符合异步要求, 第一个readFile(’./test5.txt’)会阻塞第二个readFile(’./test6.txt’)的异步加载,因为await会等待readFile(’./test5.txt’)返回后才执行下面的代码。 由于await后面接受的是一个promise,所以,我们可以使用promise的all方法或者race来组合多个promise异步为一个promise,或者也可以这样修改:
1
2
3
4
5
6
7
8
9
10
11
12 1async function asy() {
2 const promise1 = readFile('./test5.txt');
3 const promise2 = readFile('./test6.txt');
4
5 const result1 = await promise1;
6 const result2 = await promise2;
7 console.log(result1);
8 console.log(result2);
9}
10asy();
11
12
为了解决回调地狱问题:
比如:
1
2
3
4
5
6
7
8
9
10
11
12
13 1setp1(function(res)){
2 setp2(function(res)){
3 setp3(function(res){
4 setp4(function(res){
5 setp5(function(res){
6 //终于执行完了
7 });
8 });
9 });
10 });
11});
12
13
解决方法:异步串行执行
1
2
3
4
5
6
7
8
9
10
11
12
13 1var async = require('async')
2async.series([
3 function(callback){
4 fs.readFile('./test5.txt','utf8',callback);
5 },
6 function(callback){
7 fs.readFile('./test6.txt','utf8',callback);
8 }
9],function(err,res){
10 console.log(res);
11})
12
13
-
异步并行执行parallel()方法
1
2
3
4
5
6
7
8
9
10
11
12
13 1var async = require('async')
2async.parallel([
3 function(callback){
4 fs.readFile('./test5.txt','utf8',callback);
5 },
6 function(callback){
7 fs.readFile('./test6.txt','utf8',callback);
8 }
9],function(err,res){
10 console.log(res);
11})
12
13
- 异步调用的依赖处理waterfall()方法
依赖:前一个结果是后一个调用的输入参数。
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 1//./test1.txt文件内容:./test2.txt
2//./test2.txt文件内容:./test3.txt
3//./test3.txt文件内容:./test4.txt
4
5var async = require('async')
6async.waterfall([
7 function(callback){
8 fs.readFile('./test1.txt','utf8',function(err,content){
9 callback(err,content);
10 });
11 },
12 function(arg1,callback){
13 fs.readFile(arg1,'utf8',function(err,content){
14 callback(err,content);
15 });
16 },
17 function(arg1,callback){
18 fs.readFile(arg1,'utf8',function(err,content){
19 callback(err,content);
20 });
21 }
22],function(err,res){
23 console.log(res);
24})
25
26
三、总结
异步编程到这里就告一段落了,从开始了解异步I/O引出异步编程。从我们最开始使用的回调函数,再到我们理解事件的发布/订阅模块,再然后的promise/Deferred、q,以及最后的Generator函数和async/await,他们都是在不同时期为了解决异步编程而提出的解决方案,而我们也在探索中,也逐渐完善js异步编程的体验,从最开始的回调地狱,到promise的链式处理,再到后来的Generator函数和async/await可以让我们以同步的方式书写异步代码 ,希望本文能带给你一些知识的积累。