2015年就已经出现的ES6新增语法中的Promise,我一直没有时间好好研究。看了不少Promise的使用教程和原理解析,总觉得有些晦涩难懂或者不够全面,看完也没法徒手写一段使用Promise的代码,所以决定自己重新学习,并在这里简单分享自己的学习笔记。
Promise 是什么?
根据Promises/A+定义的标准:
A promise represents the eventual result of an asynchronous operation. The primary way of interacting with a promise is through its then method, which register callbacks to receive either a promise’s eventual value or the reason why the promise cannot be fulfilled.
promise对象表示异步操作的最终结果。使用promise的 主要方式是通过then方法,这个方法注册回调以接收promise的最终值或者无法fulfilled的原因。这段话对于经验丰富的人来说比较容易理解,对经验稍少的人来说,可能就显得过于抽象(当然,我翻译得可能不够准确)。初次接触Promise,我建议可以先不理会这些概念。
Promise首先是一个构造函数,相当于静态语言中的“类”,和Array、RegExp、Object是同一量级的数据类型(也就是函数)。
那么为什么要新增这一个构造函数呢?
到ECMAScript5这个版本中,js已经很强大了,React、Vue、Angular都是基于ECMAScript5开发的,前端开发已然面貌一新,为什么要多此一举?
先别急,让我们先假设一个场景。比如,我们要在页面上展示小猪佩琦一天的生活。其主要过程为:醒来->起床->穿衣服->刷牙->吃早餐->去学校->上课。最终的展示效果如下图

我们可以放7个img标签在html文件上,但是,一般情况下,这样做无疑是回到刀耕火燎的时代。那么还有什么办法?一般来说,采用异步加载的方式是首选。通过new Image() 创建一个对象,拿到图片,再渲染到页面上。可是一共有7张图片,而且它们需要按照固定的次序展示,那应该怎么做呢?毫无疑问,最终的代码会如下图所示。

像这种层层嵌套的回调函数就是令人闻风丧胆的“回调地狱”。Promise就是为解决这个问题而出现的。
也许上面的代码看起来似乎不是那么恐怖,而且运行起来没有问题。但请注意,这一段代码的格式已经很规范,并且命名清晰明了。想象一下,如果你的程序出现了一个错误,你怎么去调试?需要花多长时间?这段代码你写着写着会不会混乱?当然,如果你有时间也没问题,但是隔一个月再来看这段代码,你看得懂吗?或者这个项目让别人接手,别人能看懂吗?
当然,说得再多不如直接上代码,请看下图:

对比两张图,使用的Promise的代码整整少了8行,而且清晰明了,非常简洁、优化。这里先不谈两段代码执行过程的区别。Promise毫无疑问在可阅读性上碾压传统的开发模式。
下面,我们继续深入地谈谈Promise。根据文章开头,Promises/A+的定义:
promise对象表示异步操作的最终结果
- 什么是异步操作?
要理解什么是异步操作,首先要明白代码是怎样被解释并且执行的。
比如下面这一段代码的执行过程,我简单地在旁边标注了一下。
和异步操作相反地是同步操作,大部分情况下,代码地执行过程是同步地,也就是一行一行地往下执行,如上图。
异步操作无非就是这三类:网络请求(new XMLHttpRequest)、定时器(setTimeout、setInterval)和事件(addEventListener、元素的onclick等)。不过,在日常开发中,promise的使用场景一般都和网络请求相关。可是我们可以看到promise原理剖析类的文章,Demo里一般都会用到setTimout这个函数。为什么呢?当然是因为这样就不用涉及到后台开发了。

也许上面地代码看上去像是同步地,因为代码地执行过程是很快地。所以不妨把上面地1000改成5000试试看。
- 异步操作的结果指的是什么?
这里可以举一个简单的例子。比如,你给你的朋友发了个消息“请借我30块买喜茶”,发完了之后,你继续看书、打游戏、敲代码,不会什么也不干一直等着朋友的回复。而朋友看到消息后直接转账给你或者回复“没钱,不借”就是异步操作的“结果”,当然朋友也可能根本不理会你,这也是异步操作的“结果“。
new Promise(),相当于告诉Promise,我要给朋友发消息“请借我30块买喜茶”,那么Promise在实例化的过程中,会执行这个任务。可是朋友究竟会什么时候回复,回复什么,这个取决于朋友,没有谁可以保证。反正最终一定会有个结果的,无论是收到转账、拒绝或者不回复。那么我们怎么处理这些结果呢?如果朋友转账了,要怎么做?如果朋友拒绝了,怎么办?如果朋友不回复,怎么处理?这就涉及到promise实例中的各种方法了。!
Promise的基本使用
应该如何使用Promise?下面我们根据一个案例,尝试使用它。
首先假设一个场景:
你突然想喝喜茶,可是没有钱,打算向朋友借钱。
实例化Promise
你决定给一个朋友发微信消息“求求你借我30块买喜茶,谢谢!”。用Promise应该怎么做呢?Promise本身的意思是“承诺,诺言”,你可以把它想像成一个小跟班儿,一个助理。给朋友发消息这种行为我们直接告诉它就可以了,让它替我们完成。给朋友发送消息,其实就是发送一个网络请求,如下图的sendMsg函数。我们把这个函数作为参数传给Promise。

resolve和reject的含义
代码执行到这儿,其实已经把网络请求发送出去了。可是,小助理发送了消息之后呢?它想知道自己的工作怎样才算结束,不管是成功地达到了你的期望,还是失败了,总而言之,都算结束。那么你可以告诉它,要是朋友回了消息,不管回的内容是什么都算成功地结束了,要是你被对方删除好友,虽然失败,但也算是结束了。Promise自己提供了两个函数(resolve 和 reject )给你分别对应这两种情况。可以修改代码至下图,记住,new Promise()的时候传入的参数只要是个函数就可以了,不管是匿名函数体还是存储着函数地址的变量。

then方法
小助理发送了消息,自己也知道了结果,那么知道了就算了吗?当然不能,要是借到钱了,可以顺便让小助理把茶买回来,要是发现自己被删好友,可以让小助理顺便也把那个人给删了。这个时候就需要用到Promise实例(记住,是)的then方法了。让小助理买茶,我们用函数bugTea表示,让小助理删好友,我们用函数deleteFriend表示。不过你可能不止一个助理,我们要保证“借钱买茶”的是同一个助理,所以我们首先需要用变量 promise 存储new Promise() 的返回值,也就是一个Promise的实例。如下图:

不过小助理收到的回复也不一定就是朋友的转账,朋友有可能回复“怎么那么多人喝喜茶?”,这种情况不可能让小助理直接去买茶,所以可以先看看朋友回复的消息,要是朋友转账了,那就可以让小助理去买茶了。那么在代码实现上,我们可以在resolve()的时候,把收到的回复传进去。这样的话,在buyTea里面我们就可以加个判断,看看是不是收到了朋友的转账。如图:
resolve和reject方法

同样的,哪怕是被删了好友,我们也可以看看具体的消息,再确认一遍,让自己彻底绝望,万念俱灰地让小助理把好友删了。其代码实现和前一种情况是一样的,把内容传到reject()中,这样在删除好友前可以再仔细确认一遍,看看是不是真的被删除好友了。

finally方法
每完成一项任务,小助理要在自己的任务清单记录自己的工作。不管删除好友还是买茶,还是都没干,反正对小助理来说,“给一个朋友发微信消息‘求求你借我30块买喜茶,谢谢!’”这个任务结束了,它就完成了一项任务。这个时候,调用finally方法就可以了。

调用then方法的时候,直接传入匿名函数体也可以。

catch方法
至于promise实例的catch方法,在一定程度上可以被调用then方法时传入的第二个函数替代。catch函数里的代码什么情况下会被执行?就是Promise的状态变为rejected以后,也就是(reject()的情况);
Promise.race()和Promise.all()
Promise这个构造函数本身还提供了一些API给我们使用。比如,Promise.all()、Promise.race()、Promise.allSettled()、Promise.resolve()、Promise.reject()。那么这些api有什么作用,以及使用场景是什么,我们一起来了解一下。
Promise.race()
首先我们假设一个场景:
你无比迫切地想喝一杯喜茶,让一个小助理问你的一个朋友借钱,可能等了半天得到的只是拒绝,这样实在不划算,你完全可以多问几个朋友借钱,谁先转账就用谁的,要是有一个人率先拒绝了,那么不管剩下的人是转账还是拒绝,你通通都不管了,因为你生气了,不想再喝。race的本身就有“竞速比赛”的含义,所以显而易见,用Promise.race()这个API就足以实现上述场景所需要实现的功能。不过这次的代码我们需要稍微封装一下,因为向多个人发送借钱的消息的功能是没什么区别的。


大家可以很明显的看到,其实这个API的使用方式只是简单的把几个Promise实例作为参数传入进去,那么哪个实例最先得到结果,Promise.race()得到的就是哪一个Promise实例的结果。
Promise.all()
其实这个API我们在文章的一开头就已经出现了。下面我们重新描述一下这个场景:
小猪佩琦的生活很规律,固定不变,她每天的生活内容都是:醒来->起床->穿衣服->刷牙->吃早餐->去学校->上课。我们的任务是用图片有序地展示小猪佩奇每天的生活内容。看第一小节我们就知道了,如果拿到一个小猪佩琦醒来的图片后再请求代表她起床的图需要等很久,我们需要的是按次序展示,并不是按照次序请求。Promise.all()很好地解决了这个难题。

看Promise.all()的使用方式不难发现,它和Promise.race()很相似,都是将一些Promise实例作为参数传进去,不过区别在于,Promise.race()拿到的结果是第一个完成的Promise实例的结果,而Promise.all()的结果是所有Promise的结果,这些结果被存储在一个数组里面,每个数组的索引对应一开始传入的Promise实例的位置。
Promise的源码实现
接下来,我们可以尝试使用ES5的语法实现Promise.
简单版本
根据Promises/A+的描述,我们不难知道Promise本身有三种状态:pending、fulfilled和rejected,Promise正是根据这三种状态控制自身的执行过程。Promise的初始状态是pending, 而resolve和reject这两个函数是用来修改Promise的状态的,其中resolve会将Promise的状态修改为fulfilled, reject会将Promise的状态修改为rejected。那么我们可以首先简单实现Promise的这些设计。

同时,我们知道,Promise实例有then方法,then方法本身注入的函数参数正是拿到结果后的处理程序。加上这个功能后,代码实现如下:

锵~一个简单的Promise实现已经完成了!