本节书摘来自华章出版社《angularjs深度剖析与最佳实践》一书中的第2章,第2.10节,作者 雪狼 破狼 彭洪伟,更多章节内容可以访问云栖社区“华章计算机”公众号查看
1.生活中的一个例子
promise解决的是异步编程的问题,对于生活在同步编程世界中的程序员来说,它可能比较难于理解,这也构成了angular入门门槛之一,本节将用生活中的一个例子对此做一个形象的讲解。
假设有一个家具厂,而它有一个vip客户张先生。
有一天张先生需要一个豪华衣柜,于是,他打电话给家具厂说:“我需要一个衣柜,回头做好了给我送来”,这个操作就叫$q.defer(),也就是延期。因为这个衣柜不是现在要的,所以张先生这是在发起一个可延期的请求。
家具厂接下了这个订单,给他留下了一个回执号,并对他说:“我们做好了会给您送过去,放心吧”。这叫作promise,也就是给了张先生一个“承诺”。
这样,这个defer算是正式创建了,于是他把这件事记录在自己的日记上,并且同时记录了回执号,这个变量叫作deferred,也就是已延期事件。
现在,张先生就不用再去想着这件事了,该做什么做什么,这就是“异步”请求的含义。
假设家具厂在一周后做完了这个衣柜,并如约送到了张先生家(包邮哦,亲),这就叫作deferred.resolve(衣柜),也就是“问题已解决,这是您的衣柜”。而这时候张先生只要取出一下这个“衣柜”参数就行了。而且,这个“邮包”中也不一定只有衣柜,还可以包含别的东西,比如厂家宣传资料、产品名录等。整个过程中轻松愉快,谁也没等谁,没有浪费任何时间。
假设家具厂在评估后发现这个规格的衣柜我们做不了,那么它就需要deferred.reject(理由),也就是“我们不得不拒绝您的请求,因为……”。拒绝没有时间限制,可以发生在给出承诺之后的任何时候,甚至可能发生在快做完的时候。而且拒绝时候的参数也不仅仅限于理由,还可以包含一个道歉信,违约金之类的。总之,你想给他什么就给他什么,如果你觉得不会惹恼客户,那么不给也没关系。
假设家具厂发现,自己正好有一个符合张先生要求的存货,它就可以用$q.when(现有衣柜)来兑现给张先生的承诺。于是,这件事立刻解决了,皆大欢喜。张先生可不在乎你是从头做的还是现有的成品,只要达到自己的品质要求就满意了。
假设这个家具厂对客户格外的细心,它还可以通过deferred.notify(进展情况)给张先生发送进展情况的“通知”。
这样,整个异步流程圆满完成!无论成功还是失败,张先生都没有往里面投入任何额外的时间成本。
好,我们再扩展一下这个故事:
张先生又来订货了,这次他分多次订了一张桌子,三把椅子,一张席梦思。但他不希望今天收到个桌子,明天收到个椅子,后天又得签收一次席梦思,而是希望家具厂做好了之后一次性送过来,但是他当初又是分别下单的,那么他就可以重新跟家具厂要一个包含上述三个承诺的新承诺,这就是$q.all([桌子承诺,椅子承诺,席梦思承诺]),这样,他就不用再关注以前的三个承诺了,直接等待这个新的承诺完成,到时候只要一次性签收了前面的这些承诺就行了。
2.回调地狱和promise
通过上面这个生活中例子,相信作为读者的你已经了解到了异步和promise的方式。为什么我们需要promise呢?
javascript是一门很灵活的语言,由于它寄宿在浏览器中以事件机制为核心,所以在我们的javascript编码中存在很多的回调函数。这是一个高性能的编程模式,所以它衍生出了基于异步i/o的高性能nodejs平台。但是如果不注意我们的编码方法,那么我们就会陷入“回调地狱”,也有人称为“回调金字塔”。嵌套式的回调地狱,代码将会变得像意大利面条一样。如下边的嵌套回调函数一样:
这样嵌套的回调函数,让我们的代码的可读性变得很差,而且很难于调试和维护。所以为了降低异步编程的复杂性,开发人员一直寻找简便的方法来处理异步操作。其中一种处理模式称为promise,它代表了一种可能会长时间运行而且不一定必须完成的操作的结果。这种模式不会阻塞和等待长时间的操作完成,而是返回一个代表了承诺的(promised)结果的对象。它通常会实现一种名叫then的方法,用来注册状态变化时对应的回调函数。
promise在任何时刻都处于以下三种状态之一:未完成(pending)、已完成(resolved)和拒绝(rejected)三个状态。以commonjs promise/a 标准为例,promise对象上的then方法负责添加针对已完成和拒绝状态下的处理函数。then方法会返回另一个promise对象,以便于形成promise管道,这种返回promise对象的方式能够让开发人员把异步操作串联起来,如then(resolvedhandler, rejectedhandler)。resolvedhandler回调函数在promise对象进入完成状态时会触发,并传递结果;rejectedhandler函数会在拒绝状态下调用。
所以我们上边的嵌套回调函数可以修改为:
async1().then(async2).then(async3).catch(showerror);
这下代码看着清爽多了,我们不再需要忍受嵌套的无底深渊。
在es6的标准版中已经包含了promise的标准,很快它就将会从浏览器本身得到更好的支持。与此同时在es6的标准版中,还引入了python这类语言中的generator(迭代器的生成器)概念,它本意并不是为异步而生的,但是它拥有天然的yield暂停函数执行的能力,并保存上下文,再次调用时恢复当时的状态,所以它也被很好地运用于javascript的异步编程模型中,其中最出名的案例当属node express的下一代框架koa了。
最后还有个好消息,在es7的标准中将有可能引入async和await这两个关键词,来更大的简化我们的javascript异步编程模型。我们就可以如下的方式以同步的方式编写我们的异步代码:
angular中的promise
在angular中大量使用着promise,最简单的是$timeout的实现,我拷贝过来并加上了注释: