callback hell 和 promise 异步执行
nodejs的callback机制是比较烦人的,里面大部分方法是异步的,这个对于不熟悉的同学是非常麻烦的。
一. callback hell
首先我们来看看那 callback 机制著名的 ==callback hell(回调地狱)==:
在实际的使用当中,有非常多的应用场景我们不能立即知道应该如何继续往下执行。必须要等到上一个函数返回一些状态值才能继续判断开始业务的下一步操作。
例如:
在ajax的原生实现中,利用了onreadystatechange事件,当该事件触发并且符合一定条件时,才能拿到我们想要的数据,之后我们才能开始处理数据。代码如下:
// 简单的ajax原生实现
var url = 'https://hq.tigerbrokers.com/fundamental/finance_calendar/getType/2017-02-26/2017-06-10';
var result;
var XHR = new XMLHttpRequest();
XHR.open('GET', url, true);
XHR.send();
XHR.onreadystatechange = function() {
if (XHR.readyState == 4 && XHR.status == 200) { //这里必须判断是否符合条件再往下写业务逻辑。
result = XHR.response;
console.log(result);
}
}
如果这个时候,我们还需要做另外一个ajax请求,这个新的ajax请求的其中一个参数,得从上一个ajax请求中获取,这个时候我们就不得不如下这样做:
var url = 'https://hq.tigerbrokers.com/fundamental/finance_calendar/getType/2017-02-26/2017-06-10';
var result;
var XHR = new XMLHttpRequest();
XHR.open('GET', url, true);
XHR.send();
XHR.onreadystatechange = function() {
if (XHR.readyState == 4 && XHR.status == 200) {
result = XHR.response;
console.log(result);
// 伪代码
var url2 = 'http:xxx.yyy.com/zzz?ddd=' + result.someParams;
var XHR2 = new XMLHttpRequest();
XHR2.open('GET', url, true);
XHR2.send();
XHR2.onreadystatechange = function() {
...
}
}
}
当出现第三个ajax(甚至更多)仍然依赖上一个请求的时候,我们的代码就变成了一场灾难。这场灾难,往往也被称为 ==回调地狱==
按照 Node.js 的模式如下:
var fs = require(“fs");
2 fs.readFile(filename, "binary", function(err, file){
3 if(err){
4 //异常情况
5 }else{
6 //正常情况
// 接着在这里写你的业务代码,依次这样层层嵌套,越套越复杂。
7 }
8 });
二. Node.js 异步执行特征
node使用异步IO和事件驱动(回调函数)来解决高并发的问题。 > 一般来说,高并发解决方案会提供多线程模型,为每个业务逻辑提供一个线程,通过系统线程切换来来弥补同步I/O调用的时间开销。像apache,是一个请求一个线程。
而node.js使用的是单线程模型,对所有I/O都采用异步的请求方式,避免频繁的上下文切换,在node.js执行的时候维护着一个事件队列;程序在执行时进入事件循环等待下一个事件到来,每个异步I/O请求完成后都会被推送到事件队列中的等待执行。
Dome1:
比如说: 对于一个简单的数据库访问操作,传统方式是这样实现的
res = db.query('SELECT * from some_table');
res.output();
代码执行到第一行的时候线程会阻塞,等待query返回结果,然后继续处理。由于数据库查询、磁盘读写、网络通信等原因(所谓的I/O)阻塞时间会非常大(相对于CPU始终频率)。对于高并发的访问,一方面线程长期阻塞等待,另一方面为了应付新情求而不断添加新线程,会浪费大量系统资源,同时线程的增加也会也会占用大量的CPU时间来处理内存上下文切换。看看node.js怎么处理:
db.query('SELECT * from some_table', function(res) {
res.output();
});
query的第二个参数是一个回调函数,进程执行到db.query的时候不会等待结果返回,而是直接继续执行下面的语句,直到进入事件循环。当数据库执行结果返回的时候会将事件发送到事件队列,等到线程进入事件循环后才会调用之前的回调函数。
node.js的异步机制是基于事件的,所有的I/O、网络通信、数据库查询都以非阻塞的方式执行,返回结果由事件循环来处理。node.js在同一时刻只会处理一个事件,完成后立即进入事件循环检查后面事件。这样CPU和内存在同一时间集中处理一件事,同时尽量让耗时的I/O等操作并行执行。
Dome2:
关于 node event loop 的机制实例:
1. var fs = require("fs");
2. fs.readFile('input.txt', 4. function (err, data) {//异步执行,这个地方没有等待执行结束就已经打印了"程序执行结束",然后打印data数据
if (err) return console.error(err);
console.log(data.toString());
});
3. console.log("程序执行结束!");
如上的代码所示:
- 程序运行到该处时候,引用完后继续往下走。
- 到2处时,node 发现了一个异步的 I/O 操作,众所周知 I/O 操作十分耗费资源,node 是单线程的它真的很不想干这件该死的事情!所以它直接交给了 => libuv, 并给它一个回调函数,也就是标签 4 的地方,这个回调是在 c/c++ 底层处理完之后,libuv 就会去调用这个回调。 ===> 但在交给 libuv 的过程中,程序会继续往下走,不会等你返回的信息。也就到了3的地方了。
- 异步执行到了这里打印结果。所以你会先看到 “程序执行结束”,然后才会看见 读取的文件数据内容!
总之 node 一直把难搞的交给别人去搞,等别人搞完了,只执行一个回调而已。所以说node不适合做大量计算的工作,比如你写个while(true){}整个程序就蹦了。
node就是喜欢小计算多并发,它处理起来真的有优势,不服不行。
三. Promise
promise 主要用于解决 callback hell 回调地狱的问题,当然还可以为了我们的代码更加具有可读性和可维护性,我们需要将数据请求与数据处理明确的区分开来。
当我们想要确保某代码在谁谁之后执行时,我们可以利用函数调用栈,将我们想要执行的代码放入回调函数中。
// 一个简单的封装
function want() {
console.log('这是你想要执行的代码');
}
function fn(want) {
console.log('这里表示执行了一大堆各种代码');
// 其他代码执行完毕,最后执行回调函数
want && want();
}
fn(want); // => 这里表示执行了一大堆各种代码
// => 这是你想要执行的代码
我们还可以利用队列机制:
function want() {
console.log('这是你想要执行的代码');
}
function fn(want) {
// 将想要执行的代码放入队列中,根据事件循环的机制,我们就不用非得将它放到最后面了,由你自由选择
want && setTimeout(want, 0);
console.log('这里表示执行了一大堆各种代码');
}
fn(want);
用 promise对象:
这里是带参数传递的。
function want() {
console.log('这是你想要执行的代码');
}
function fn(want) {
console.log('这里表示执行了一大堆各种代码');
// 返回Promise对象
return new Promise(function(resolve, reject) {
if (typeof want == 'function') {
resolve(want);
} else {
reject('TypeError: '+ want +'不是一个函数')
}
})
}
fn(want).then(function(want) {
want();
})
fn('1234').catch(function(err) {
console.log(err);
})
等价于:
function want() {
console.log('这是你想要执行的代码');
}
function fn(want) {
console.log('这里表示执行了一大堆各种代码');
// 返回Promise对象
return new Promise(function(resolve, reject) {
if (typeof want == 'function') {
resolve(want);
} else {
reject('TypeError: '+ want +'不是一个函数')
}
}).then(function(want) {
want();
}).catch(function(err) {
console.log(err);
});
}
fn(want);
fn('1234');
打印结果:(仔细理解其中的异步执行特性)
这里表示执行了一大堆各种代码
这里表示执行了一大堆各种代码
这是你想要执行的代码
TypeError: 1234不是一个函数
关于 promise 的基础知识:
1. Promise对象有三种状态:
- pending: 等待中,或者进行中,表示还没有得到结果
- resolved(Fulfilled): 已经完成,表示得到了我们想要的结果,可以继续往下执行
- rejected: 也表示得到结果,但是由于结果并非我们所愿,因此拒绝执行
这三种状态不受外界影响,而且状态只能从pending改变为resolved或者rejected,并且不可逆。
在Promise对象的构造函数中,将一个函数作为第一个参数。而这个函数,就是用来处理Promise的状态变化。
new Promise(function(resolve, reject) {
if(true) { resolve() };
if(false) { reject() };
})
上面的resolve和reject都为一个函数,他们的作用分别是将状态修改为resolved和rejected。
2. Promise对象中的then方法
以接收构造函数中处理的状态变化,并分别对应执行。then方法有2个参数,第一个函数接收resolved状态的执行,第二个参数接收reject状态的执行。
function fn(num) {
return new Promise(function(resolve, reject) {
if (typeof num == 'number') {
resolve();
} else {
reject();
}
}).then(function() {
console.log('参数是一个number值');
}, function() {
console.log('参数不是一个number值');
})
}
fn('hahha');
fn(1234);
then方法的执行结果也会返回一个Promise对象。因此我们可以进行then的链式执行,这也是解决回调地狱的主要方式。
function fn(num) {
return new Promise(function(resolve, reject) {
if (typeof num == 'number') {
resolve();
} else {
reject();
}
})
.then(function() {
console.log('参数是一个number值');
})
.then(null, function() {
console.log('参数不是一个number值');
})
}
fn('hahha');
fn(1234);
then(null, function() {}) 就等同于catch(function() {})
3. Promise中的数据传递
var fn = function(num) {
return new Promise(function(resolve, reject) {
if (typeof num == 'number') {
resolve(num);
} else {
reject('TypeError');
}
})
}
fn(2).then(function(num) {
console.log('first: ' + num);
return num + 1;
})
.then(function(num) {
console.log('second: ' + num);
return num + 1;
})
.then(function(num) {
console.log('third: ' + num);
return num + 1;
});
// 输出结果
first: 2
second: 3
third: 4