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

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)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,110评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,443评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,474评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,881评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,902评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,698评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,418评论 3 419
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,332评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,796评论 1 316
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,968评论 3 337
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,110评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,792评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,455评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,003评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,130评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,348评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,047评论 2 355

推荐阅读更多精彩内容

  • Promise 对象 Promise 的含义 Promise 是异步编程的一种解决方案,比传统的解决方案——回调函...
    neromous阅读 8,707评论 1 56
  • 本文适用的读者 本文写给有一定Promise使用经验的人,如果你还没有使用过Promise,这篇文章可能不适合你,...
    HZ充电大喵阅读 7,311评论 6 19
  • JavaScript里通常不建议阻塞主程序,尤其是一些代价比较昂贵的操作,如查找数据库,下载文件等操作,应该用异步...
    张歆琳阅读 2,759评论 0 12
  • Promise的含义:   Promise是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和...
    呼呼哥阅读 2,170评论 0 16
  • Promise对象是一种解决异步问题的方法,还有的解决方案是asyns 和 await (es7) 这么是目前的终...
    站在大神的肩膀上看世界阅读 1,265评论 0 6