导读
在异步编程中,Promise扮演了举足轻重的角色,它解决了ajax请求过程中的回调地狱的问题,令代码更具可读性。下面的介绍中,笔者会通过一些片段代码,加上一些其自己的理解带大家一起重新温故一下Promise为编程所带来的便利。
Promise是抽象异步处理对象以及对其进行各种操作的组件;
Promise很重要!很重要!很重要!对,要强调三遍,一定要好好掌握。
实例(假如此处你还不是很了解,没关系,先留个印象):
var promise = new Promise((resolve,reject) => {
复制代码
**注意⚠️:**本文中的函数表达式均采用ES6的箭头函数表达式的语法,你若还不是很清楚,请自行查阅(参考阮一峰ECMAScript 6入门)[1]。
回调函数
刚刚我们说,Promise解决了ajax请求过程中的回调地狱的问题,那么回调(函数)是什么,为什么要用到回调(函数),我们一起回顾一下。
回调函数(callback function),也被称作高阶函数。
就是把一个函数B作为参数(注意:是作为参数)传入“另一个函数A”中,然后这个函数B在“另一个函数A”中调用,那么这个函数B,就叫回调函数。函数A执行完以后执行函数B,这个过程就叫做回调。
注意:回调函数不是立即就执行。它是在另一个函数执行完成之后被调用,即在包含的函数体中指定的地方“回头调用”。
也就是:A(主函数)让 B(参数)做事,B做着做着,信息不够,不知道怎么做了,就需要A告诉他,这时,A到外面获取信息,待A执行完毕后拿到了所需信息,再回过头来调用B。
网上也有一个通俗易懂的例子帮助理解回调函数:
你到一个商店买东西,刚好你要的东西没有货,于是你在店员那里留下了你的电话,过了几天店里有货了,店员就打了你的电话,然后你接到电话后就到店里去取了货。在这个例子里,你的电话号码就叫回调函数,你把电话留给店员就叫登记回调函数,店里后来有货了叫做触发了回调关联的事件,店员给你打电话叫做调用回调函数,你到店里去取货叫做响应回调事件。
回调函数的使用场景:主要是需要将父函数的执行结果通知给回调函数进行处理时使用。
//回调函数举例1
复制代码
可见此处主函数runAsyncMain执行的过程中,按顺序本应先执行回调函数,但输出结果却是后输出的回调函数内容,这说明,主函数不用等回调函数执行完再执行后续的语句,可以接着执行自己的代码,等回调函数准备好,再执行回调函数。所谓的异步加载也不过如此,当然,异步与回调并没有直接的联系,回调只是异步的一种实现方式。
为什么需要Promise?
介绍完回调函数,要回到Promise的主场了。程序执行过程中,有非常多的应用场景我们不能立即知道应该如何继续往下执行,例如很重要的ajax请求
。通俗来说,由于网速的不同,可能你得到返回值的时间也不同,这时我们就需要等某个结果出来了之后才知道怎么样继续下去,例如下方的回调函数案例:
// 需求:当一个ajax结束后,得到的值,需要作为另外一个ajax的参数被使用(即该参数得从上一个ajax请求中获取)
复制代码
当上述需求中出现第三个ajax(甚至更多)仍然依赖上一个请求的时候,代码就会变成一场灾难。也就是我们常说的回调地狱
。
这时,我们可能会希望:
-
让代码变得更具有可读性和可维护性,减轻一层层套用数据和请求的现象;
-
将请求和数据处理明确的区分开。
这时Promise
就要闪亮登场了,Promise中强大的then方法,可以解决刚刚出现的恐怖的回调地狱
问题,并且让代码更优雅。
别急,我们先从文档中最基础的 API 入手。
Promise的API
1、constructor (构造函数属性)
Promise
本身也是一个构造函数
,需要通过这个构造函数创建一个新的Promise
对象作为接口,使用new
来调用Promise
的构造器来进行实例化,所以这个实例化出来的新对象:具有constructor属性,并且指针指向他的构造函数Promise。
var promise = new Promise((resolve, reject) => {
复制代码
2、Instance Method (实例方法)
promise.then()
Promise对象中的promise.then(resolve,reject)
实例方法,可以接收构造函数中处理的状态变化,通过此方法,设置了其值在resolve(成功)/reject(失败)时调用的回调函数,并分别对应执行。
promise.then(onFulfilled, onRejected)
复制代码
then方法有2个参数(都是可选参数,此参数是个回调函数):
-
resolve成功时
onFulfilled
会被调用; -
reject失败时
onRejected
会被调用。
promise.then
成功和失败时都可以使用,并且then方法的执行结果也会返回一个Promise对象
。
promise.catch()
另外在只想对异常进行处理时可以采用promise.then(undefined, onRejected)
这种方式,只指定reject时的回调函数即可。不过这种情况下 promise.catch(onRejected)
应该是个更好的选择。
promise.catch(onRejected)
复制代码
注意:在IE8及以下版本,使用promise.catch()
的代码,会出现identifier not found的语法错误。(因为catch
与ECMAScript的保留字[2](Reserved Word)有关,在ECMAScript 3中保留字是不能作为对象的属性名使用的。)
解决办法:不单纯的使用catch
,而是使用then
来避免这个问题。
//then和catch方法 举例
复制代码
3、Static Method (静态方法)
像Promise
这样的全局对象还拥有一些静态方法,后文中会有详细解释。
Promise.resolve()
Promise.resolve()方法返回一个已给定值解析后的新的Promise对象,从而能继续使用then的链式方法调用。
Promise.reject()
Promise.reject()方法和Promise.resolve()方法一样。
Promise.all()
Promise.all()方法的作用是将多个Promise
对象实例包装,生成并返回一个新的Promise
实例。
Promise.race()
Promise.race()与Promise.all()相反。
Promise的状态 (Fulfilled、Rejected、Pending)
Promise的精髓是“状态”,用维护状态、传递状态的方式来使得回调函数能够及时调用。
用new Promise
实例化的promise对象有以下三个状态:
-
"unresolved" -
Pending
| 既不是resolve也不是reject的状态。等待中,或者进行中,表示Promise刚创建,还没有得到结果时的状态; -
"has-resolution" -
Fulfilled
| resolve(成功)时。此时会调用onFulfilled
; -
"has-rejection" -
Rejected
| reject(失败)时。此时会调用onRejected
。
关于下面这三种状态的读法,其中左侧为在ES6 Promises规范中定义的术语,而右侧则是在Promises/A+中描述状态的术语。
上图的意思是promise对象的状态,从_Pending_转换为_Fulfilled_或_Rejected_之后, 这个promise对象的状态就不会再发生任何变化,会一直保持这个结果。
当promise的对象状态发生变化时,用.then
来定义只会被调用一次的函数。
Promise的使用
1、创建Promise对象
前面很多次强调,Promise本身就是一个构造函数,所以可以通过new创建新的Promise对象:
var p = new Promise((resolve, reject) => {
复制代码
我们执行了一个异步操作
,也就是setTimeout,1秒后,输出“执行完成”,并且调用resolve方法。但是只是new了一个Promise对象,并没有调用它,我们传进去的函数就已经执行了。为了避免这个现象产生,所以我们用Promise的时候一般是包在一个函数中,需要的时候去运行这个函数。
如果你对执行的先后顺序还不理解,推荐阅读文章事件的循环机制(Event loop)[3]前文中我们也曾多次提到异步加载,所以此概念应熟记于心。
异步任务
:指不进入主线程、而进入"任务队列"(task queue)的任务
,只有等主线程任务执行完毕,“任务队列”开始通知主线程,请求执行任务,该任务才会进入主线程执行。异步加载例如:你烧壶水要冲咖啡,可是水要10分钟才能烧开,此时,你转身去写了个小程序,等10分钟后水好了,才回来继续冲咖啡的活动,中间你去做了很多别的有意义的事情,也并没有耽误冲咖啡这项任务,这就是异步。
也可以理解为可以改变程序正常执行顺序的操作就可以看成是异步操作。例如setTimeout和setInterval函数。
同步任务
:指在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;同步加载例如:你烧壶水要冲咖啡,可是水要10分钟才能烧开,此时,你就等啊等啊等,等了10分钟水好了,才继续冲咖啡的活动,中间的过程就是等待,啥都不干,这就是同步。
推荐参考文章:彻底理解同步、异步和事件循环(Event Loop)[4]
你会发现,异步加载的举例和上文中强调的回调函数例子很像,但刚刚也强调了,异步与回调并没有直接的联系,回调只是异步的一种实现方式(再次重复,加深理解)。
你会不会以为异步像是多线程操作那样并列执行程序?我想说,千万不要这样想!js就是单线程,没有多线程一说,所以不存在并行,即便是异步,也是单线程,只不过是放在了异步队列里,对,是队列,倘若你不是很理解,那么请前去了解一下事件循环机制中的****宏任务和微任务(promise就是微任务,settimeout是宏任务,非常不错的一篇文章)的区别,它们所在的队列是不同的,看过之后,相信你会对promise有更深刻的了解。
2、封装Promise对象
function asyncFunction(num) {
复制代码
我们刚刚讲到,then方法的执行结果也会返回一个Promise对象
,得到一个结果。因此我们可以进行then的链式执行,接收上一个then返回回来的数据并继续执行,这也是解决回调地狱
的主要方式。
3、Promise的链式操作和数据传递
下面我们就来看看.then和.catch这两个方法返回的到底是不是新的promise对象。
var aPromise = new Promise(resolve => {
复制代码
===
是严格相等比较运算符,我们可以看出这三个对象都是互不相同的,这也就证明了then
和catch
都返回了和调用不同的promise对象。我们通过下面这个例子进一步来理解:
// 1: 对同一个promise对象同时调用 `then` 方法
复制代码
写法1 中并没有使用promise的方法链方式,这在Promise中应该是极力避免的写法。这种写法中的then
调用几乎是同时开始执行的,而且传给每个then
方法的value
值都是100
。
写法2 则采用了方法链的方式将多个then
方法调用串连在了一起,各函数也严格按照 resolve → then → then → then 的顺序执行,并且传给每个then
方法的value
的值都是前一个promise对象通过return
返回的值,实现了Promise的数据传递。
强调:promise的链式操作实现了数据的传递,promise非链式操作的方法无法实现数据传递。
4、通过Promise封装ajax解决回调地狱问题
刚刚在开篇(【为什么需要Promise】 这一节),通过一个ajax的例子,引出了回调地狱的概念,强调了通过回调函数方式解决多级请求都依赖于上一级数据时所引发的问题。下面我们通过刚刚学习过的Promise内容(特别是.then的链式数据传递)对上面的ajax数据依赖的案例进行重写:
var url = 'XXXXX';
复制代码
new Promise写法的快捷方式
1、Promise.resolve
new Promise(resolve => {
复制代码
另:Promise.resolve
方法另一个作用就是将thenable对象转换为promise对象。
ES6 Promises提到了Thenable这个概念,简单来说它就是一个非常类似promise的东西。就像我们有时称具有.length
方法的非数组对象为类数组(Array like)一样,thenable指的是一个具有.then
方法的对象。
因为:类库没有提供Promise
的实现,用户通过Promise.resolve(thenable)
来自己实现了Promise
,并且,作为Promise使用的时候,需要和Promise.resolve(thenable)
一起配合使用,将thenable对象转换promise对象:
var promise = Promise.resolve($.ajax('/json/comment.json'));// => promise对象
复制代码
在此不再对Thenable进行过多赘述,可自行了解。
2、Promise.reject
new Promise((resolve,reject) => {
复制代码
Promise.all()
Promise.all
接收一个promise对象的数组作为参数
,当这个数组里的所有promise对象全部变为resolve或reject状态的时候,它才会去调用.then
方法。也就是说:Promise的all方法提供了异步操作的能力,并且在所有异步操作执行完后才执行回调。
// `delay`毫秒后执行resolve
复制代码
这说明timerPromisefy
会每隔1、32、64、128ms都会有一个promise发生resolve
行为,返回一个promise对象,状态为FulFilled,其状态值为传给timerPromisefy
的参数,并且all会把所有异步操作的结果放进一个数组中传给then。
从上述结果可以看出,传递给Promise.all
的promise并不是一个个的顺序执行的,而是同时开始、并行执行
的。
Promise.race()
all方法的效果实际上是「谁跑的慢,以谁为准执行回调」,那么相对的就有另一个方法「谁跑的快,以谁为准执行回调」,这就是race方法,这个词本来就是赛跑的意思。race的用法与all一样,接收一个promise对象数组为参数。
Promise.all
在接收到的所有的对象promise都变为FulFilled或者Rejected状态之后才会继续进行后面的处理,与之相对的是Promise.race
只要有一个promise对象进入FulFilled或者Rejected状态的话,就会继续进行后面的处理。
// `delay`毫秒后执行resolve
复制代码
上面的代码创建了4个promise对象,这些promise对象会分别在1ms、32ms、64ms和128ms后变为确定状态,即FulFilled,并且在第一个变为确定状态的1ms后,.then
注册的回调函数就会被调用,这时候确定状态的promise对象会调用resolve(1)
因此传递给value
的值也是1,控制台上会打印出1
来。
promise的基本使用原理以及它在实际应用中为我们解决的问题,在上述过程中已经介绍完了,你是否理解了呢?学习是一个反复阅读,反复加深印象的过程,加油牢牢掌握这一知识点,在vue、react等框架的使用中,也会频繁用到有关promise的知识,下面一起来检测一下对promise的认知结果吧。
小练习
下面内容的输出结果应该是啥?
function test1() {
复制代码
温馨提示:这里没有为then
方法指定第二个参数(onRejected)。
补充1:对比 callback → Promise → async/await
javascript的异步发展历程,从callback,到Promise对象、Generator函数,不停地优化程序上的编写方式,但又让人觉得不是很彻底,随即又有了之后的async/await的异步编程方式,让异步编程变得更像同步代码,增强了代码的可读性,甚至很多人评价async/await是异步操作的终极解决方案,接下来简单介绍一下这三种方式各自的优缺点:
1.callback(回调):本文开篇也提及了回调函数虽然好理解,但只对于简单的异步程序,callback是可以胜任的,但是在ajax需要被多次调用时使用起来还是会产生很多问题:
-
高耦合,让程序变得难以维护;
-
并且错误捕捉要通过人工的设置判断来进行。
2.Promise:ES6提供的构造函数Promise的实现是要基于callback的,解决了异步执行的问题:
-
通过Promise.then()链式调用的方法,解决了回调函数层层嵌套(回调地狱)的问题,让代码和操作都变得更加简洁;
-
可以统一通过Promise.catch()方法对异常进行捕获,无需再像callback那样,为每个异步操作添加异常处理;
-
Promise.all()方法可以对异步操作进行并行处理,同时执行多个操作。
但Promise也存在缺点:
-
当处于未完成状态时,无法确定目前处于哪一阶段;
-
如果不设置回调函数,Promise内部的错误不会反映到外部;
-
Promise一旦新建它就会立即执行,无法中途取消。
ES6中,还有一个generator函数,以前一个函数中的代码要么被调用,要么不被调用,不存在能暂停的情况,generator函数让代码可以中途暂停、异步执行,它与Promise的结合使用,类似于async/await(见下文)效果的代码。
整个Generator函数就是一个封装的异步任务的容器。它的语法是在函数名前加个*号,在异步操作需要暂停的地方,都用yield语句注明,但仅有yield,函数是不会执行的,他需要调用next方法,指针都会向下移一个状态,直到遇到下一个yield
表达式(或return
语句)为止。
3**.async/await**:ES7中新增的异步编程方法,async/await的实现是基于 Promise的,简单而言就是**async function就是返回Promise的function,是generator的语法糖,其实async函数就是将Generator函数的星号(*)替换成 async,将yield替换成await。**很多人认为async/await是异步操作的终极解决方案:
-
改进JS中异步操作串行执行的代码组织方式,减少callback的嵌套;
-
语法简洁,更像是同步代码,也更符合普通的阅读习惯;
-
Promise中不能自定义使用try/catch进行错误捕获,但是在Async/await中可以像处理同步代码处理错误。
综上异步编程的演变过程可见,它语法目标,其实就是怎样让它更像同步编程。
如果你想要更详细的了解Generator和async/await异步操作方式,请查阅MDN中的相关文档。同时推荐一篇通俗易懂的文章:www.lazycoffee.com/articles/vi…
补充2:Promise 的实现原理
一起回顾一下, 前面我们介绍了promise的来由、解决的问题、常用和重点的方法、以及promise的使用方法和应用场景,你是不是也很好奇,没有promise的话,我们要如何模拟出promise呢?也就是,promise是如何实现的?
回顾一下Promise的使用过程:
**1.**首先要知道,Promise对象有三个状态:Pending(进行中)
、Fulfilled(已成功)
、Rejected(已失败)
,所以需要Promise设置三个状态值;
**2.**Promise存在resolve和reject两个回调函数作为自身参数:new Promise((resolve, reject){});,通过判断步骤1中的三个状态值,来确定输出哪个方法,例如:
-
promise处于Fulfilled状态时,就输出resolve对应的方法;
-
Rejected状态时就输出reject方法。
**3.**Promise的then方法要接收两个参数:promise.then(onFulfilled, onRejected),onFulfilled和onRejected也必须是两个函数:
-
当Promise的状态为成功时,调用onFulfilled这个方法,其中onFulfilled方法中的参数是步骤2中promise成功状态 resolve执行时传入的值;
-
当Promise的状态为失败时,调用onRejected这个方法,其中onRejected方法中的参数是步骤2中promise失败状态reject执行时传入的值。
**4.**如果then被同一个Promise多次调用,所有onFulfilled
和onRejected
需按照其注册顺序依次回调;
5.……
**6.**当然,Promise还有.catch、.all、.race等很多方法,以及基本逻辑和规则确定后,还需要加上错误捕获、值传递等机制,例如判断步骤2中的回调函数是否为function、步骤4链式操作中,then返回的是否为一个新的Promise对象等等;
具体的Promise实现原理代码如下,若有兴趣的童鞋可参考阅读,帮助进一步加深理解:
// 判断变量否为function
复制代码
参考:
[1].阮一峰ECMAScript 6入门(es6.ruanyifeng.com/#docs/funct…)
[2].保留字(mothereff.in/js-properti…)
[3].事件的循环机制(Event loop)(www.jianshu.com/p/12b9f73c5…)~~
[4].彻底理解同步、异步和事件循环(Event Loop)(segmentfault.com/a/119000000…) (developer.mozilla.org/zh-CN/docs/…)
[5].JavaScript Promise迷你书(liubin.org/promises-bo…)
[6].透彻掌握Promise的使用(www.jianshu.com/p/fe5f17327…)