前言:

在上个章节中我们介绍了 JavaScript 回调的设计,同时也介绍了回调存在的一些问题,来回顾下。

当我们只有一个异步任务的时候使用回调函数看起来还不会有什么问题。

  1. const request = require("request");
  2. request('https://www.baidu.com', function (error, response) {
  3. if (!error && response.statusCode == 200) {
  4. console.log('get times 1');
  5. });

但是,实际是我们完成一个任务通常需要多个异步操作。例如:这里有三个网络请求,第二个必须等第一个结束才能发出,第三个必须等第二个结束才能发起,如果我们使用回调就会变成这样:

  1. const request = require("request");
  2. request('https://www.baidu.com', function (error, response) {
  3. if (!error && response.statusCode == 200) {
  4. console.log('get times 1');
  5. request('https://www.baidu.com', function(error, response) {
  6. if (!error && response.statusCode == 200) {
  7. console.log('get times 2');
  8. request('https://www.baidu.com', function(error, response) {
  9. if (!error && response.statusCode == 200) {
  10. console.log('get times 3');
  11. }
  12. })
  13. }
  14. })
  15. }
  16. });

你使用越多的回调,就会有越多的嵌套,不断缩进意大利面条似的代码。很显然,这种代码难以编写,难以理解而且难以维护。这类嵌套/缩进经常被叫做”回调地狱”。有时也被叫做”回调金字塔”,专指由于代码不断缩进所形成的金字塔形状,缩进越多金字塔形状越明显。

但这还仅仅是阅读跟维护性上的障碍,其实回调还有一个我们无法忽视的安全/可靠性问题。

TOP1

可靠性缺失

接下来我们通过一个简单的回调函数来分析下这个过程会发生什么。

  1. //1.现在立即执行的同步代码
  2. someAsync(function(){
  3. //2.未来执行的代码
  4. }) ;

我们把把第二部分代码包装在一个回调函数中然后延迟到后面执行。 这个时候就会存在一个问题代码在执行到1-2之间控制权并不在我们手上,因为很多时候someAsync(..)是第三方库提供的一个方法并不是我们自己写的,并且我们期望着它:

  1. 不会太早调用我的回调函数
  2. 不会太迟调用我的回调函数
  3. 会给我的回调提供必要的参数
  4. 在我的回调失败的时候会提醒我

这里真正的问题是由于回调引起的控制转移。在你的程序的前半部分,你控制着程序的执行。现在你转移了控制权,someAsync(..)控制了你剩余程序什么时候返回以及是否返回。控制转移表明了我们的代码和其他人的代码之间存在过度信任关系。

我们会把这种常见现象叫做 Duct-tape 意思就是像胶带一样把很多第三方的库连接起来。 这似乎好像是一个恶性循环,虽然我们可以在someAsync 中添加状态跟踪机制通过补丁的方式解决安全问题,但是实际上我们只是处理了信任列表许多项目中的一项。 因为我们无法避免要使用 Duct-tape 连接其他的第三方库,而这些第三方库也可能会 Duct-tape 其他的第三方库。

因此我们必须要思考有没有一种模式能让我们解决可靠性丢失问题,轻松的来表达异步流程控制。

而我们今天要介绍的 Promise 就能给我们提供这种模式。在我解释 Promise 是怎么工作之前,先来解释一些它背后的概念问题。

TOP2

一家小面馆

当你走进你最喜爱的小面馆,走到前台要了一碗炸酱面。收银员告诉你一共8块钱然后你把钱给她。她会给你一张写着序列号的小票,接下来你就找座位等待着炸酱面。

当我们听到广播响起:“请512号取餐”。正好是你的号码。你走到前台用小票换来了美味的炸酱面。

刚才发生的是一个对于 Promise 很好的比喻。你走到前台开始一个业务,但是这个业务不能马上完成。所以,你得到一个在迟些时候完成业务(你的食物)的promise(小票)。一旦你的食物准备就绪,你会得到通知然后你第一时间用你的promise(小票)换来了你想要的东西。

换句话说,带有序列号的小票就是对于一个未来结果的承诺。

完成事件

想想上面调用someAsync(..)的例子。如果你可以调用它然后订阅一个事件,当这个调用完成的时候你会得到通知而不是传递一个回调给它,这样不更好吗?

  1. var listener = someAsync(..) ;
  2. listener.on("success",function(data){
  3. //keep going now !
  4. }) ;

同时也监听调用失败的事件。

  1. var listener = someAsync(..) ;
  2. listener.on("fail",function(data){
  3. //keep going now !
  4. }) ;

现在我们重新获得了程序的控制权而不是通过给第三方库传递回调来转移控制权。

TOP3

Promise

Promises就像是一个函数在说 “我这有一个事件监听器,当我完成或者失败的时候会被通知到。” 我们看看它是怎么工作的:

  1. function someAsyncThing(){
  2. var p = new Promise(function(resolve,reject){
  3. //at some later time,call 'resolve()' or 'reject()'
  4. }) ;
  5. return p ;
  6. }
  7. var p = someAsyncThing() ;
  8. p.then(
  9. function(){
  10. //success happened
  11. },
  12. function(){
  13. //failure happened
  14. }
  15. ) ;

你只需要监听then事件,然后通过知道哪个回调函数被调用就可以知道是成功还是失败。

TOP4:

逆转

通过 Promise,我们重新获得了程序的控制权而不是通过给第三方库传递回调来转移控制权。这是javascript中异步控制流程表达上一个很大的进步。

但是在这里我们任然需要传递回调,所以你要注意Promise 并不是通过移除回调来解决 “回调地狱” 的问题。 在一些情况下,你甚至需要比以前更多的回调。同时,根据你如何编写你的代码,你可能仍然需要把Promise 嵌套在别的 Promise 中!

  1. new Promise(resolve => {
  2. console.log('a1');
  3. resolve();
  4. })
  5. .then(() => {
  6. console.log('a2')
  7. // 简称 p2
  8. new Promise(resolve => {
  9. console.log('b1');
  10. resolve();
  11. })
  12. .then(() => console.log('b2'))
  13. .then(() => console.log('b3'))
  14. })
  15. .then(() => console.log('a3'))
  16. .then(() => console.log('a4'))
  17. // a1 a2 b1 b2 a3 b3 a4 b4

批判性地看,Promise 所做的只是改变了你传递回调的地方。

本质上,如果当我们把回调传递给拥有良好保证和可预测性的中立 Promise 机制,你就能重新获得了对于后续程序能很稳定并且运行良好的可靠性。标准 Promise 机制有以下这些保证:

  • 对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。
  • 一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了。
  • 如果promise返回了成功的信息,那么你绑定在成功事件上的回调会得到这个消息。
  • 如果发生了错误,promise会收到一个带有错误信息的错误通知。

总结:

“回调地狱” 是函数嵌套产生的代码阅读性维护性难的问题。它也是关于控制转移的问题,由于把控制权交给一个我们不能信任的第三方而产生的对我们的程序失去控制的现象。

Promise 逆转了这个情况,它使得我们重新获得控制权。相比传递回调给第三方函数,函数返回一个Promise 对象,我们可以使用它来监听函数的成功或失败。在promise我们仍然使用回调,但是重要的是标准的 Promise 机制使我们可以信任它们行为的正确性。我们不需要想办法来处理这些可靠性问题。

Promise 的最重要的特点就是它把我们处理任何函数调用的成功或者失败的方式规范成了可预测的形式,
如果没有可靠性,那么你就跟使用普通的回调一样了。你必须谨慎地编写那些涉及到异步调用第三方库的代码。你必须自己来解决状态跟踪的问题然后确保第三方库不会出问题。

Promise也有一些缺点。
首先,无法取消Promise,一旦新建它就会立即执行,无法中途取消。
其次,如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。
第三,当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

但瑕不掩瑜 Promise 给JavaScript 异步流程控制 带来了全新的规范,在一个遵循 promise规范的系统中,我们不用在担心可靠性问题,因为它会按照 Promise 机制来执行。

所以我们需要promise 这种控制反转的能力,但是也迫切需要一种能更好组织代码的方式。 这部分内容我们会在下节课研究看看有没有什么破局的办法。

关于Promise 的更多API用法。

https://segmentfault.com/a/1190000000586666
https://segmentfault.com/a/1190000000591382
https://segmentfault.com/a/1190000000593885
https://zhuanlan.zhihu.com/p/28315360