前言
前两天在吃饭时,同事忽然抛出一个问题,Promise 的真正意义是啥? 对话场景如下:
他:Promise 的意义是啥?
我:为解决回调地狱,增强了代码可读性,改善了代码结构
他:这是大家都知道的,仅仅是为了这个吗? Promise 解决了什么回调不能解决的问题,如果仅仅是为了回调地狱和可读性,Promise从社区走向ES6标准这么长的历程是不是有点小题大作?
我:宿主(浏览器,V8)发起的叫做宏观任务(Macro Task)比如事件,setTimeout。JavaScript引擎本身发起的叫做微观任务。Promise 正是赋予了 js 引擎发起微观任务(Micro Task)的能力。
他:so what ? 这只是一种实现方式的不同,Promise 成为标准前,大部分实现它的库的都是用 setTimeout 模拟的,至少说明绝大多数用到 Promise 的场景都可以用 setTimeout 替换。况且 Promise 的一大堆 then 也不见得多好看。。
我 : 咳咳,解决回调地狱至少是 Promise 从社区兴起的一条重要主线,正因为 Promise 还是有繁琐的地方,所以 ES7 又推出了 async/await,作为 Promise 的一个语法糖。从回调地狱 -> Promise -> async/await,不正是体现了可读性可维护性这条主线吗?
他: 感觉还是分量不足。。。
我:Promise 还提供了很多牛逼的 api,比如 race, all 等,算是一整套处理异步的解决方案吧。
他: 有点道理,不过觉得还是没有 get 到那个叫人虎躯一震的终极意义。。
我:看来本次午饭面试我以失败告终了。。。?
他:哎。。。探讨探讨,回去再查查
Promise 深层意义
吃好饭趁休息时间,好好的 google 了一把。大部分文章中对 Promise 的讲解都是基于上述我们对话中的角度,比如 Promise 的含义,使用,API介绍,宏观任务/微观任务。可以总结为是什么和怎么用。
关于为什么,有两篇文章说的很好:
O’REILLY出版的 你不知道的JavaScript 关于 Promise 的部分
You’re Missing the Point of Promises
Promise 是可信任的
言下之意即回调是有信任风险的,举个栗子:
假设我们在开发一个交易系统。当用户点击“确定”购买时,需要调用由某个分析追踪公司提供的第三方函数以便跟踪这个交易。
可能是为了提高性能,这是一个异步追踪的工具,这意味着我们需要传入一个回调函数,支付和展示感谢页面的逻辑在回调中做。
analytics.trackPurchase(purchaseData, function(){
chargeCreditCard(); // 支付
displayThankyouPage(); // 展示感谢页面
});
看起来没有任何问题。然而半年后的某一天,老板忽然打电话过来,让你赶紧到办公室来。
原来一位客户购买了一次,信用卡却被刷了五次,他很生气,这可以理解。客服已经道歉并启 动了退款流程。
通过分析日志,原来是那个分析工具在出于某种原因(比如升级时的bug)把回调调用了五次而不是一次。类似于:
if (purchaseData) {
callback(); callback();....callback();
}
在下次更新前,第三方库中的这个 bug 会一直存在。哪怕问题修复了,我也不会再信任这个库了。因此不得不自己动手搞一个防御性编程,加一些临时代码绕过这个 bug. 比如:
var tracked = false;
analytics.trackPurchase( purchaseData, function(){
if (!tracked) {
tracked = true;
chargeCreditCard();
displayThankyouPage();
}
});
接下来看 Promise 是如何构建信用的:
还以上文的交易系统为例,如果这个第三方异步追踪工具用 Promise 实现,那么基本的调用方式应该是这样:
analytics.trackPurchase(purchaseData).then(function() {
chargeCreditCard();
displayThankyouPage();
});
乍一看无非是把回调函数放到了 then 当中,似乎与回调方式并无什么不同。但是仔细想想你就会发现,Promise 的写法绝对不会发生反复支付那种 bug。原因就是回调实现中,回调函数的控制权在第三方手中,而 Promise 中回调的控制权在我们自己手上。
Promise 这种模式通过可信任的语义(resolve or reject)把回调作为参数传递,使得这种行为更可靠更合理。通过把回调的控制反转反转回来,我 们把控制权放在了一个可信任的系统(Promise)中,这种系统的设计目的就是为了使异步编码更清晰。
真正的回调地狱
上面例子中第三方库出错的原因是对回调的多个并发调用。这只是一种错误情形。头脑风暴一下,错误类型其实可能会有很多:
- 调用回调过早(在追踪之前)
- 调用回调过晚(或没有调用)
- 调用回调的次数太少或太多(上小节遇到的问题)
- 没有把所需的环境 / 参数成功传给你的回调函数
- 吞掉可能出现的错误或异常
- …
对于无法信任的工具的每个回调,我们将不得不做大量的防御性编程 (workaround) 来避免这些问题的发生。这才是真正的回调地狱!
小结
Promise 解决了我们因只用回调的代码而备受困扰的控制反转问题。
Promise 并没有摈弃回调,只是把回调的安排转交给了一个位于我们和其他工具之间的可信任的中介机制。
Promise 链也开始提供(尽管并不完美)以顺序的方式表达异步流的一个更好的方法,这有助于我们的大脑更好地计划和维护异步 JavaScript 代码。