1、异步编程和回调函数
网络数据传输和磁盘读写等操作是十分耗时的,JavaScript引擎会把这些耗时的操作陷入其他线程,从而让主线程能够一马平川地跑下去,浏览器也不会因为脚本等待耗时操作的相应而卡死。
这对用户很好,但对程序员来说就不那么友好了,因为异步API都是通过接收回调函数来把异步操作的处理结果导出的。
举个栗子:node.js的https类的get方法:
const https = require('https');
https.get('https://encrypted.google.com/', (res) => {
console.log('statusCode:', res.statusCode);
console.log('headers:', res.headers);
});
get方法接收的第一个参数是一个url,node会向这个url发送一个https请求,并生成一个response对象。
第二个参数就是所谓的回调函数,开发者可以通过定义这个函数来接收get方法产生的response对象,get函数的定义大概是这样的:
get(url, callback) {
// do something 异步操作, 生成res对象
callback(res);// 在生成了response对象后将这个对象传入回调函数
}
如果后面的代码需要用到response,那么这些代码都需要写进这个回调函数里,这样做会使代码看起来很难看。
尤其是当回调函数体内还有异步操作要执行时,会出现回调套回调的情况,对逻辑比较复杂的业务甚至会出现七八个回调函数套在一起的情况,圈内管这种情况叫“回调地狱”,会严重影响代码的可读性和可维护性。
2、Promise
如果你读到这,相信你已经理解了异步和回调的概念,Promise为我们提供了一种异步编程的优雅写法。异步编程给我们带来的困扰在于,我们无法立刻得到异步操作的结果,只能把大量的业务代码套进回调函数里。
而使用Promise,我们可以立刻得到异步操作的结果,或者,更准确地说,我们可以立刻得到异步操作的状态,然后在异步操作完成时获得它的结果。
2.1 什么是Promise?
在回答这个问题之前,首先来看看我们怎么使用Promise
var p = new Promise((resolve,reject) => {
console.log(resolve, reject)
}) // ƒ () { [native code] } ƒ () { [native code] }
可以看到,首先,我们使用new关键字创建Promise对象,这意味着Promise是JavaScript的一个内置的类,这个类的构造函数接收一个函数,并为该函数提供两个参数,resolve和reject,这两个参数是JavaScript的原生函数。
然后,让我们来打印一下这个Promise类的实例p,来看看它长什么样:
console.log(p);
// __proto__: Promise
// [[PromiseStatus]]: "pending"
// [[PromiseValue]]: undefined
我们可以看到三条信息:
1、这个实例继承自Promise类(废话)
2、PromiseStatus 属性的值为 pending
3、PromiseValue 属性为undefined
其中,PromiseStatus 是promise的状态,状态可以有两种情况:
1、一种是上面这种情况,pending,表明promise还没有得到任何结果,处于等待状态
2、另一种是settled,promise已经得到结果,而settled又有两种情况:solved和rejected,
那么是什么决定了promise的状态呢? PromiseValue又是什么东西呢?看下面这段代码:
var p1 = new Promise((resolve,reject) => {
resolve("hello, Promise");
});
var p2 = new Promise((resolve,reject) => {
reject("bye, Promise");
});
console.log("p1: ", p1);
console.log("p2: ", p2);
// p1:
// [[PromiseStatus]]: "resolved";
// [[PromiseValue]]: "hello, Promise"
// p2:
// [[PromiseStatus]]: "rejected"
// [[PromiseValue]]: "bye, Promise"
p1的PromiseStatus值为resolved,PromiseValue值为"hello, Promise",
p2的PromiseStatus值为rejected, PromiseValue值为"bye, Promise",
可以观察到,在调用了resolve函数后,promise的状态变为resolved, promise value值为传给resolve函数的参数。如果调用了reject函数,promise的状态变为rejected,promise value为传给reject函数的参数。
通过上面的试验,我们可以对Promise有一个大概的了解:
1、promise是一个对象
2、promise对象初始化时接收一个函数,并为这个函数传入resolve和rejecte两个方法
3、promise对象有两个属性:PromiseStatus 和 PromiseValue
4、在调用resolve方法后,PromiseStatus的值为resolved,PromiseValue的值为resolve的参数;
5、在调用reject方法后,PromiseStatus的值为rejected,PromiseValue的值为rejecte的参数;
6、在调用resolve或reject方法前,PromiseStatus的值为pending,PromiseValue的值为undefined
2.2 Promise 有啥用?
了解了什么是Promise,那么它有什么用?还是举https.get的例子,只不过这次用promise来写
const https = require('https');
https.get('https://encrypted.google.com/', (res) => {
console.log('statusCode:', res.statusCode);
console.log('headers:', res.headers);
}).on('error', (e) => {
console.error(e);
});
// 以上代码等效于
var p = new Promise((resolve, reject) => {
https.get('https://encrypted.google.com/', (res) => {
if(!_.isEmpty(res)){
resolve(res)
} else {
reject("出错了")
}
})
})
p.then(res =>{
console.log('statusCode:', res.statusCode);
console.log('headers:', res.headers);
}).catch(console.error);
来看看发生了什么,我们把https.get包进了传给Promise的函数中,并在拿到get函数的异步操作得到结果res后,将res传给solve函数,如果res为空,则调用reject,并传入参数”出错了“。然后用then来接收resolve的参数,用catch来接收reject的参数。
https.get函数的回调函数触发之前,p的PromiseStatus为pending,promiseValue为undefined, 回调函数运行,我会判断res对象是否为空,若不为空,则将res传给resolve函数,使p得promiseStatus变为resolved,promisedValue变为res;若res为空,则reject。
你也许会问,这样做代码量比之前多了许多,写起来更麻烦了,我为什么要这么做?
通过这样做,我们可以把一个异步操作装进一个对象里,如同这个例子中,我们将https.get放进了promise对象p中,这样一来我们可以在任何地方通过p来访问https.get的res对象,将业务代码从回调函数中解放出来。
2.3 then和catch
promise对象有两个方法,then和catch
其中then方法接收一个回调函数作为参数,在promiseStatus为resolved时,将promiseValue值传给该回调函数
而catch方法同样接收一个回调函数作为参数,用来在promiseStatus为rejected时接收promise对象的promiseValue
var p1 = new Promise((resolve, reject) => {
resolve("Hello, Promise")
});
var p1 = new Promise((resolve, reject) => {
reject("Bye, Promise")
});
p1.then(console.log); // "Hello, Promise"
p2.catch(console.log); // "Bye, Promise"
then和catch都会返回一个promise对象,这个由then或catch返回的promise对象会以then或catch的回调函数的返回值作为promiseValue,并且promiseStatus值为resolved。
var p = new Promise((resolve, reject) => {
resolve("Hello, Promise")
});
p.then(data => data+", where are you going?").then(console.log);
// "Hello, Promise, where are you going?"
这个特性使得promise对象可以像链条一样,一个一个地链起来。
在使用promise时,reject通常用来抛出异常,而catch很自然地用来接异常,如果promise对象的promiseStatus值为rejected,则promiseValue值会跳过后面所有的then,落进遇到的第一个catch中
var p = new Promise((resolve, reject) => {
reject("Bye, Promise")
});
p.then(data => console.log(1, data)).then(data => console.log(2, data)).catch(errMsg => console.log(3, errMsg));
// 3 "Bye errMsg"
类似地,若promise对象的promiseStatus为resolved,则promiseValue会跳过后面的所有catch,落进遇到的第一个then中。
3 回调函数风格的Promise化
使用Promise有各种各样的好处,相信你一定会喜欢它,甚至希望将回调函数风格的API转为Promise,而你的确可以这么做,来看看如何得到Promise的https.get吧:
const https = require('https');
function get(url){
return new Promise((resolve, reject) => {
https.get('https://encrypted.google.com/', (error, res) => {
if(error){
reject(error);
} else {
resolve(res)
}
});
})
定义一个返回promise对象的函数,该promise对象的resolve值为res,如果出错,则reject(error)