美文网首页JavaScript 进阶营
Promise-优雅地进行JavaScript异步编程

Promise-优雅地进行JavaScript异步编程

作者: 火燎菠萝 | 来源:发表于2018-06-16 22:23 被阅读10次

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);
});
// 以上代码等效于
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)

相关文章

网友评论

    本文标题:Promise-优雅地进行JavaScript异步编程

    本文链接:https://www.haomeiwen.com/subject/nhgpeftx.html