理解 Promise

Promise 规范

这里只列举规范中的大致内容,详细内容请查看 Promises/A+ 中文 ,这是ES6 Promises的前身,是一个社区规范,它和 ES6 Promises 有很多共通的内容。

  1. 状态 Promise 的初始状态是 Pending ,状态只能被转换为(Resolved)FulfilledRejected,状态的转换不可逆。
  2. then 必须有 then 方法,接收两个可选函数参数onFulfilledonRejectedthen 方法必须返回一个新的 Promise 对象,为了保证 then 中回调的执行顺序,回调必须使用异步执行。
  3. 兼容 不同的 Promise 的实现必须可以互相调用

具体标准的实现将在 中篇 - 手动封装 中详细说明

ES6 Promise API

如果你对 Promise的使用 还不是很了解,可参考阅读以下资料:

这里只对ES6 Promise API做简要说明

实例方法

  • .then(resolvedFn, rejectFn)
    为Promise实例添加状态改变时的回调,返回值是一个 新的Promise实例
  • .catch()
    .then(null, rejectFn) 的语法糖,返回值也是一个 新的Promise对象
    Promise对象的错误具有冒泡性质,错误会不断的向后传递,直到 .catch() 捕获
    正因为 thencatch 返回的都是 Promise 对象,所以才可以不断的链式调用

静态方法

  • Promise.resolve()
  1. 将现有对象转换为Promise对象
  2. 如果参数是promise实例,则直接返回这个实例
  3. 如果参数是thenabled对象(有then方法的对象),则先将其转换为promise对象,然后立即执行这个对象的then方法
  4. 如果参数是个原始值,则返回一个promise对象,状态为resolved,这个原始值会传递给回调
  5. 没有参数,直接返回一个resolved的Promise对象
  • Promise.reject()
    同上,不同的是返回的promise对象的状态为rejected

  • Promise.all()

  1. 接收一个Promise实例的数组或具有Iterator接口的对象,
  2. 如果元素不是Promise对象,则使用Promise.resolve转成Promise对象
  3. 如果全部成功,状态变为resolved,返回值将组成一个数组传给回调
  4. 只要有一个失败,状态就变为rejected,返回值将直接传递给回调
  5. all() 的返回值也是新的Promise对象
  • Promise.race()
  1. 同上,区别是,只要有一个Promise实例率先发生变化(无论是状态变成resolved还是rejected)都触发then中的回调,返回值将传递给回调
  2. race()的返回值也是新的Promise对象

Polyfill和扩展类库

Polyfill

只需要在浏览器中加载Polyfill类库,就能使用IE10等或者还没有提供对Promise支持的浏览器中使用Promise里规定的方法。

calvinmetcalf/lie 非常简洁的 promise 库,中篇中的手动封装实现就是参考了这个库
jakearchibald/es6-promise 兼容 Promises/A+ 的类库, 它只是 RSVP.js 的一个子集,只实现了Promises 规定的 API。
yahoo/ypromise 这是一个独立版本的 YUI 的 Promise Polyfill,具有和 ES6 Promises 的兼容性。

Promise扩展类库

Promise扩展类库除了实现了Promise中定义的规范之外,还增加了自己独自定义的功能。

kriskowal/q 类库 Q 实现了 Promises 和 Deferreds 等规范。 它自2009年开始开发,还提供了面向Node.js的文件IO API Q-IO 等, 是一个在很多场景下都能用得到的类库。
petkaantonov/bluebird这个类库除了兼容 Promise 规范之外,还扩展了取消promise对象的运行,取得promise的运行进度,以及错误处理的扩展检测等非常丰富的功能,此外它在实现上还在性能问题下了很大的功夫。

注意
在项目中,有可能两个不同的模块使用的是两个不同的Promise类库,那么在大部分的Promise的实现中,都是遵循 Promise/A+ 标准和兼容ES6 Promise接口的,也是不同的Promise的实现是可以互相调用的,如何调用,将在下面说明。

错误用法及误区

当作回调来用 Callback Hell

|

<pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 14px; margin: 0px; padding: 10px; color: rgb(77, 77, 76); background: rgb(247, 247, 247); line-height: 1.6; border: none; width: 344px;">loadAsync1().then(function(data1) {
loadAsync2(data1).then(function(data2) {
loadAsync3(data2).then(okFn, failFn)
});
});
</pre>

|

Promise是用来解决异步嵌套回调的,这种写法虽然可行,但违背了Promise的设计初衷
改成下面的写法,会让结构更加清晰

|

<pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 14px; margin: 0px; padding: 10px; color: rgb(77, 77, 76); background: rgb(247, 247, 247); line-height: 1.6; border: none; width: 328px;">loadAsync1()
.then(function(data1) {
return loadAsync2(data1)
})
.then(function(data2){
return loadAsync3(data2)
})
.then(okFn, failFn)
</pre>

|

没有返回值

|

<pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 14px; margin: 0px; padding: 10px; color: rgb(77, 77, 76); background: rgb(247, 247, 247); line-height: 1.6; border: none; width: 297.6px;">loadAsync1()
.then(function(data1) {
loadAsync2(data1)
})
.then(function(data2){
loadAsync3(data2)
})
.then(res=>console.log(res))
</pre>

|

promise 的神奇之处在于让我们能够在回调函数里面使用 return 和 throw, 所以在then中可以return出一个promise对象或普通的值,也可以throw出一个错误对象,但如果没有任何返回,将默认返回 undefined,那么后面的then中的回调参数接收到的将是undefined,而不是上一个then中内部函数 loadAsync2 执行的结果,后面都将是undefined。

没有Catch

|

<pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 14px; margin: 0px; padding: 10px; color: rgb(77, 77, 76); background: rgb(247, 247, 247); line-height: 1.6; border: none; width: 328px;">loadAsync1()
.then(function(data1) {
return loadAsync2(data1)
})
.then(function(data2){
return loadAsync3(data2)
})
.then(okFn, failFn)
</pre>

|

这里的调用,并没有添加catch方法,那么如果中间某个环节发生错误,将不会被捕获,控制台将看不到任何错误,不利于调试查错,所以最好在最后添加catch方法用于捕获错误。

添加catch

|

<pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 14px; margin: 0px; padding: 10px; color: rgb(77, 77, 76); background: rgb(247, 247, 247); line-height: 1.6; border: none; width: 328px;">loadAsync1()
.then(function(data1) {
return loadAsync2(data1)
})
.then(function(data2){
return loadAsync3(data2)
})
.then(okFn, failFn)
.catch(err=>console.log(err))
</pre>

|

catch()与then(null, fn)

在有些情况下catch与then(null, fn)并不等同,如下

|

<pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 14px; margin: 0px; padding: 10px; color: rgb(77, 77, 76); background: rgb(247, 247, 247); line-height: 1.6; border: none; width: 344px;">ajaxLoad1()
.then(res=>{ return ajaxLoad2() })
.catch(err=> console.log(err))
</pre>

|

此时,catch捕获的并不是ajaxLoad1的错误,而是ajaxLoad2的错误,所以有时候,两者还是要结合起来使用:

|

<pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 14px; margin: 0px; padding: 10px; color: rgb(77, 77, 76); background: rgb(247, 247, 247); line-height: 1.6; border: none; width: 520.8px;">ajaxLoad1()
.then(res=>{ return ajaxLoad2() }, err=>console.log(err))
.catch(err=> console.log(err))
</pre>

|

断链 The Broken Chain

|

<pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 14px; margin: 0px; padding: 10px; color: rgb(77, 77, 76); background: rgb(247, 247, 247); line-height: 1.6; border: none; width: 528.8px;">function loadAsyncFnX(){ return Promise.resolve(1); }
function doSth(){ return 2; }

function asyncFn(){
var promise = loadAsyncFnX()
promise.then(function(){
return doSth();
})
return promise;
}

asyncFn().then(res=>console.log(res)).catch(err=>console.log(err))
// 1
</pre>

|

上面这种用法,从执行结果来看,then中回调的参数其实并不是doSth()返回的结果,而是loadAsyncFnX()返回的结果,catch 到的错误也是 loadAsyncFnX()中的错误,所以 doSth() 的结果和错误将不会被后而的then中的回调捕获到,形成了断链,因为 then 方法将返回一个新的Promise对象,而不是原来的Promise对象。

改写如下

|

<pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 14px; margin: 0px; padding: 10px; color: rgb(77, 77, 76); background: rgb(247, 247, 247); line-height: 1.6; border: none; width: 528.8px;">function loadAsyncFnX(){ return Promise.resolve(1); }
function doSth(){ return 2; }

function asyncFn(){
var promise = loadAsyncFnX()
return promise.then(function(){
return doSth();
})
}

asyncFn().then(res=>console.log(res)).catch(err=>console.log(err))
// 2
</pre>

|

穿透 Fall Through

|

<pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 14px; margin: 0px; padding: 10px; color: rgb(77, 77, 76); background: rgb(247, 247, 247); line-height: 1.6; border: none; width: 266.4px;">new Promise(resolve=>resolve(8))
.then(1)
.catch(null)
.then(Promise.resolve(9))
.then(res=> console.log(res))
// 8
</pre>

|

这里,如果then或catch接收的不是函数,那么就会发生穿透行为,所以在应用过程中,应该保证then接收到的参数始终是一个函数。

长度未知的串行与并行

并行执行

|

<pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 14px; margin: 0px; padding: 10px; color: rgb(77, 77, 76); background: rgb(247, 247, 247); line-height: 1.6; border: none; width: 451.2px;">getAsyncArr()
.then(promiseArr=>{
var resArr = [];
promiseArr.forEach(v=>{
v().then(res=> resArr.push(res))
})
return resArr;
})
.then(res=>console.log(res))
</pre>

|

使用forEach遍历执行promise,在上面的实现中,第二个then有可能拿到的是空的结果或者不完整的结果,因为,第二个then的回调无法预知 promiseArr 中每一个promise是否都执行完成,那么这里可以使用 Promise.all 结合 map 方法去改善

|

<pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 14px; margin: 0px; padding: 10px; color: rgb(77, 77, 76); background: rgb(247, 247, 247); line-height: 1.6; border: none; width: 382.4px;">getAsyncArr()
.then(promiseArr=>{
return Promise.all(promiseArr);
})
.then(res=>console.log(res))
</pre>

|

如果需要串行执行,那和我们可以利用数据的reduce来处理串行执行

|

<pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 14px; margin: 0px; padding: 10px; color: rgb(77, 77, 76); background: rgb(247, 247, 247); line-height: 1.6; border: none; width: 582.4px;">var pA = [
function(){return new Promise(resolve=>resolve(1))},
function(data){return new Promise(resolve=>resolve(1+data))},
function(data){return new Promise(resolve=>resolve(1+data))}
]

pA.reduce((prev, next)=>prev.then(next).then(res=>res),Promise.resolve())
.then(res=>console.log(res))
// 3
</pre>

|

Promise.resolve的用法

Promise.reoslve 有一个作用就是可以将 thenable 对象转换为 promise 对象。

thenable 对象,指的是一个具有 .then 方法的对象。
要求是 thenable 对象所拥有的 then 方法应该和 Promise 所拥有的 then 方法具有同样的功能和处理过程。
一个标准的 thenable 对象应该是这样的

|

<pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 14px; margin: 0px; padding: 10px; color: rgb(77, 77, 76); background: rgb(247, 247, 247); line-height: 1.6; border: none; width: 289.6px;">var thenable = {
then: function(resolve, reject) {
resolve(42);
}
};
</pre>

|

使用 Promise.resolve转换

|

<pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 14px; margin: 0px; padding: 10px; color: rgb(77, 77, 76); background: rgb(247, 247, 247); line-height: 1.6; border: none; width: 389.6px;">Promise.resolve(thenable).then(function(value) {
console.log(value); // 42
});
</pre>

|

同样具有标准的thenable特性的是 不同的实现Promise标准的类库,所以 ES6 Promise 与 Q 与buldbird 的对象都是可以互相转换的。

jQueyr的defer对象转换为ES6 Promise对象

|

<pre style="overflow: auto; font-family: consolas, Menlo, "PingFang SC", "Microsoft YaHei", monospace; font-size: 14px; margin: 0px; padding: 10px; color: rgb(77, 77, 76); background: rgb(247, 247, 247); line-height: 1.6; border: none; width: 551.2px;">Promise.resolve($.ajax('api/data.json')).then(res=>console.log(res)))
</pre>

|

但也不是所有thenable对象都能被成功转换,主要看各种类库实现是否遵循 Promise/A+标准,不过此类使用场景并不多,不做深入讨论。

最佳实践

  1. then方法中 永远 returnthrow
  2. 如果 promise 链中可能出现错误,一定添加 catch
  3. 永远传递函数给 then 方法
  4. 不要把 promise 写成嵌套

经过本篇的对Promise相关知识的理解和学习,基本上对Promise的概念和使用有了比较详细的了解,下一篇就让我们一起进入 Promise 的源码世界看一看吧。

参考文章:
谈谈使用 promise 时候的一些反模式
深入理解 Promise
Promises/A+ 中文

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

推荐阅读更多精彩内容

  • Promise 对象 Promise 的含义 Promise 是异步编程的一种解决方案,比传统的解决方案——回调函...
    neromous阅读 8,698评论 1 56
  • Promise 的含义 一句话概括一下promise的作用:可以将异步操作以同步操作的流程表达出来,避免了层层嵌套...
    雪萌萌萌阅读 5,442评论 0 7
  • 在ES6当中添加了很多新的API其中很值得一提的当然少不了Promise,因为Promise的出现,很轻松的就给开...
    嘿_那个谁阅读 3,662评论 2 3
  • ES6---new Promise()讲解,Promise对象是用来干嘛的?原文链接:https://blog.c...
    小豆soybean阅读 234评论 0 0
  • 前言 本文旨在简单讲解一下javascript中的Promise对象的概念,特性与简单的使用方法。并在文末会附上一...
    _暮雨清秋_阅读 2,181评论 0 3