Promise是ES6(又称ECMA2015)一个新引入的全局对象,用于一个异步操作的最终完成(或失败)及其结果值的表示,它使得异步代码的编写更为简单。
本文为译文,原文出处: https://scotch.io/tutorials/javascript-promises-for-dummies,作者通过本文生动的介绍了Promise是什么、如何使用以及为什么使用它来编写异步代码。
简介
“假设你是一个学生,你的妈妈承诺下周给你买一部手机。”
直到下周来之前,你都不知道是否会得到手机。但结果会是其中之一:
- 你的妈妈真的给你买了
- 由于其他原因而没有买成
这就是Promise(承诺),它是一个状态机,具有3个状态,分别是:
- pending: 等待中,直到下周来之前,你不知道是否会得到新手机。
- resolved: 符合期望,你的妈妈真的买了手机给你。
- rejected: 期望落空,由于其他原因没有买成。
创建
让我们用JavaScript来陈述上面的例子:
|
|
isMomHappy
:Boolean,不买手机的理由可以有很多,这得看妈妈的心情。willIGetNewPhone
:Promise对象,它可以是resolve
(期望达成) 或者reject
(期望落空)。这里使用标准语法来定义Promise对象,具体参考MDN,大概像这样:
12// promise syntax look like thisnew Promise(/* executor*/ function (resolve, reject) { ... } );需要记住的是,当期望达成时,调用resolve(successValue),当期望落空时,调用reject(failValue)。在这个例子中,假设妈妈给我们买了一部新手机,因此调用了resolve(phone);否则我们调用reject(reason)。
使用
现在,我们已经有了一个Promise,接下来看看如何使用它:
|
|
- 我们调用
askMom
方法来使用之前定义的Promise(willIGetNewPhone)
。 - 一旦Promise做出响应,我们将用
.then
或者.catch
来进行相应的处理。 - 在
.then
里包含了一个function(fulfilled){ ... }
,那么fulfilled
的值是什么,根据之前Promise的定义,我们得知这个值应该是phone
,包含新手机的信息。 - 在
.catch
里也包含了一个function(error){ ... }
,根据定义,error
的值应该是reason
,是拒绝买手机的理由。
你可以在线运行这个Demo
链式调用
Promise可以被链式调用。
接下来你有了另外的想法,如果拿到新手机,就和小伙伴们炫耀一把,这时你可以创建另外一个Promise,例如:
|
|
在上面的Promise中,我们没有使用
reject
,因为它是可选的。
另外,我们还可以对它进行简化:
|
|
接下来让我们链式地调用它:
|
|
这里需要注意:只有在willIGetNewPhone
达成,才会启动showOff
。就像你没有拿到手机,也不会去跟小伙伴炫耀了,对吧?
异步执行
Promise是异步执行的,我们添加一些日志来测试一下:
|
|
这里会输出是什么?更合理的应该是:
|
|
实际输出的结果为:
|
|
这个结果证实了Promise内部是异步执行的。好比在等待新手机的到来之前,并不会停止你现在的生活一样。同样,在Promise等待时,代码并不会阻塞,而是继续往下执行,如果需要在等待结果后处理的,请在.then
中去完成。
(ES5/ES6/ES7)
ES5
因为ES5还不支持原生Promise,你需要引入 Bluebird 或者 Q 来兼容,它们在大多数ES5环境(浏览器和Node)都可以正常工作。
ES6/ES2015-主流浏览器、NodeJSv6
ES6可以支持原生Promise,无需引入任何其他第三方库。此外,你还可以使用新特性=>
、const
和let
来进一步简化代码:
|
|
注意,所有var
被换成了const
,所有的function(resolve, reject)
替换成了(resolve, reject) =>
,这么做是有好处的。
ES7-async/await语法更简洁
ES7引入了async
和await
语法,看起来很像同步代码且符合逻辑,这比.then
和.catch
的语法更加清晰和容易理解。
使用ES7语法重写上面的例子:
|
|
- 每当需要返回一个
Promise
,在函数前加一个async
,像async function askMom()
。 - 每当需要调用一个
Promise
,在前面加一个await
,像let phone = await willGetNewPhone
和let message = await showOff(phone)
。 - 使用
try { ... } catch(error) { ... }
将会捕获到Promise中的error
和reject
。
解决哪些问题
为什么我们需要Promise?在回答这个问题之前,让我们回到原来写异步的方式,通过简单例子来说明:
实现两个函数,功能都是完成两个数值的相加,区别是:一个同步,一个异步:
两数相加的普通函数:
|
|
两数相加的异步函数,通过请求:
|
|
上面例子中,通过普通函数的形式,我们会立马得到result
。但是在异步中,需要等待结果的返回,所以无法立刻获得result
,也就是说,在处理耗时操作时,像调用API、下载/读取文件一些常见的操作时,我们都需要等待,但又不想等待的过程中主进程被阻塞,因为还有其他的事情要做。在Promise出现以前,我们的解决方法是使用Callback
,让我们修改上面的例子:
|
|
它能正常工作,既然如此,还需要Promise吗?
接下来我们尝试对相加后的结果进行后续处理,假设我们对结果进行3次的累加操作:
同步:
|
|
Demo: https://jsbin.com/barimo/edit?html,js,console
异步:
|
|
Demo: https://jsbin.com/qafane/edit?js,console
这种由 callback
嵌套另外一个 callback
组成的代码,第一眼看上去有些像金字塔,但人们通常称之为”回调地狱”,它的可读性非常差。你可以想象一下累加10次的代码长什么样。
逃离回调地狱:
让我们用Promise来改写它:
|
|
Demo: https://jsbin.com/qafane/edit?js,console
通过Promise,我们将callback
进行扁平化处理,不用再写类似回调地狱的代码了,还有更好的做法是使用ES7的async/await
,但这留给你自己去实现。
总结:
当采用Promises后,程序变得非常清楚易读。
在实际项目中,经常需要编写大量的异步代码,然而在异步的处理(异步流)中却很容易丢失控制权,Promise可以很好解决这个问题。
觉得Promise不能够很好地处理复杂工作流?还可以试试RxJs。
关于Promise
的实现,请参考https://www.promisejs.org/implementing/。