查看原文
其他

【第553期】Promise 陷阱

2016-04-18 ziczhu 前端早读课

前言

今天分享的这些坑,不知道你们有遇到过吗?前方高能,第一次贴这么多代码。


正文从这开始~


这不是一篇介绍 Promise 的文章,如果你暂时还不知道 Promise 是什么,可以参考这本非常棒的小书:。本文只介绍使用 Promise 时候容易遇到的一些坑和注意事项。


Lesson One: APromise is a Promise.

不行!說的是一輩子!差一年、一個月、一天、一個時辰...都不算一輩子!

-- 程蝶衣


承诺 (Promise) 始终都应该是承诺 (Promise),即使落空,也应该是一个失败 (Rejected) 的承诺 (Promise)


Promise 对象渐渐成为了现代 JavaScript 程序异步接口的标准返回。Promise 相对于 Callback,拥有两个先天的优势:

  • Promise 的值在确定后是不可变的。

  • Promise 确保结果一定是异步的,不会出现的问题。


If you have an APIwhich takes a callback,
and sometimes that callback is called immediately,
and other times that callback is called at some point in the future,
then you will render any code using this API impossible to reason about, andcause the release of Zalgo.


我们重点来看第二点,同样也是Callback 的一个重大缺点,就是结果太不可控了,除非我们百分之百确定这个接口是异步的,否则有可能出现上文所说的情况,这个接口一会儿是异步的(第一次网络请求),一会儿是同步的(直接返回本地 Cache),而且更糟糕的是,如果这个作者仇视社会的话,没准还会调用好几次回调,而这些都是你没法控制的(┑(Д )┍摊手)。


而这些 Callback 的缺点同样是 Promise 的卖点,但你以为用了 Promise 就大功告成了嘛: No!



好了,一个还算严谨的除法程序(原谅我用Promise 实现),做了类型校验,还做了被除数非 0 的校验,给你 3 秒钟说一下这程序有什么问题,3...2...等不及了,这个程序最大的问题在于,虽然用 Promise 不像回调那样会很明显的把异步和同步返回混淆,但一不小心,我们把校验的逻辑写成了同步的。这时候如果一味天真的少年用了我们这个强大 Promise 函数。



Tips

当我们自己着手设计一个返回 Promise对象的函数时,请尽量都采用立即返回 new Promise 的形式。



当然,如果我们的 Promise 工厂函数依赖了另一个 Promise 对象的结果的时候,也可以直接 return 那个 Promise 对象。

很多时候,由于我们的疏忽大意,一些松散的逻辑或者意料之外的输入都会让我们理想中的 Promise 返回化为泡影。但如果你把所有逻辑都写在 Promise 构造器或 Promise 对象的 then/catch 函数中的话,即使一个意外的输入导致内部抛了错,也能(绝大部分情况下)返回一个 Rejected Promise,而不是一个未捕获的错误。


所以,即使用了 Promise,也可能导致 release Zalgo 的发生,所以请你在下次写完一个 Promise 返回的函数的时候,再仔细瞅瞅,它一定会返回一个 Promise 吗?(说好的一辈子呢,混蛋(ε(#)


Lesson Two: Rejector Throw?

她習慣向左走,他習慣向右走,他們始終不曾相遇。

-- 幾米


当然,我们是在讨论使用 Promise 构造器的用法,你在 then 里面都没 reject 呢。我们在前一章说过,始终在Promise 构造器中书写逻辑的话,即使出现了意外的输入,也能绝大部分情况下返回一个Rejected Promise,好了,本章讨论的就是其他情况,坦诚说,这一点也不少见。


还是以上一个除法程序为例。



效果和之前是一模一样的,而且 throw 的用法看起来还更常见,但 reject throw 有一个本质的不同!reject 是回调,而 throw 只是一个同步的语句,如果在另一个异步的上下文中抛出,在当前上下文中是无法捕获到的。例如下面的代码,我们用 setTimeout 模拟一个异步的抛错。


果然,这个错误没有被 Promise 捕捉到,还导致了另外一个问题,我们成功通过了校验,返回了 NaN,这些都不是我们想要的结果。


当然通常你也不会写这样的代码,但我们还是有那么多的 callback-style API 啊。一不注意就可能写成下面那样。


很不幸,这个函数除非完全满足我们的预期(包含某些内容的文件),其余情况都会抛出一个我们无法 catch 到的错误,更不幸的是,这样的错误也无法用 try/catch 捕捉到,你要不小心写了这样的程序,并且只测试了通过的情况,很有可能突然的一天,你的程序就崩溃了。那时,你的内心是不是也要崩溃了呢。


当然,这种异步 throw 的作法在某些情况下也是很有用的,可以防止未知的错误被 Promise 吞掉,造成程序 Debug 的困难。例如 中的 done 函数,就是类似下面的实现。



Tips

Promise 构造器中,除非你明确知道使用 throw 的正确姿势,否则都请使用 reject



另外,在异步回调函数中,除了我们自己写的throw 语句之外,任何其他原因造成的错误都会导致抛出我们无法捕捉到的异常。例如 JSON 解析,所以,在异步回调中请千万注意,不要出现意料之外的错误抛出,所有可能的错误都请用reject 明确拒绝。

Lesson Three:Early Return

你見,或者不見我,我就在那裡。不悲不喜。

-- 倉央嘉措


前面在第一章的时候说过 Promise 的一大优点,就是结果不变性,一旦 Promise 的值确定为 fulfilled 或者 rejected 后,无论过多久,获取到的 Promise 对象的值都是一样的。



如上图所示,我们在原有程序的基础上增加了一些日志来查看 Promise 内部的执行状态。


突然感到这世界森森的恶意,不是说Promise 确定后不变嘛,怎么都 reject 还接着走。咳咳,少年,不要惊慌,我们说的是 Promise 确定后不变,不代表 reject 之后函数就不执行了啊,你们年轻人啊,还是 too young too simple,蛤蛤。


JavaScript 函数中,只有 return / yield / throw 会中断函数的执行,其他的都无法阻止其运行到结束的,这也是所谓的特性。


resolve/reject 不过只是一个回调而已,而所谓的不变性只是说,当遇到第一个 resolve/reject 后,便根据其结果给此 Promise 打上了一个 tag,并且不能更改,而后面的该干啥继续干,不干本 Promise 的事儿了。


Tips

解决这个问题的方法也很简单,就是在resolve/reject 之前加上 return 即可,跟我们平常函数中的用法一样,当然了,因为这本身就是一个普通的函数嘛。


对于这段代码来说,执行后续代码的后果是打印出多余的日志,实际情况肯定比这复杂得多,比如某个异步调用或者网络请求,甚至是一个 CPU 密集型的循环操作,我相信所有这些都不是你想要的,所以请你在resolve/reject 语句前面加上 return,除非你真的想把后续的代码一直运行到结束。


Lession Four: Backto Callback

妳相信壹切都永不會改變。然後妳離開了,壹年,兩年,當妳回來時,壹切都變了。

-- 天堂電影院


现代 Web 的很多新颖的 API 都已经采用了 Promise 作为返回,例如大家都很熟悉的,还有很让人期待的 等。然而,这并不是一篇介绍如何使用某某 API 的说明书,而是谈另外一个问题,在 Promise Callback 同时存在的宇宙上,如何写出一个同时坐拥两者的异步 API


因为在 Node.js 中,所有的原生异步 API 基本都是采用了 Error-first callbacks,甚至可以被简称了 Node-style 了,例如下面很简单的一个读取文件的例子: 


好了,我们试着简单包装一下。如果第二个参数传入了函数,就直接调用原生的 readFile。否则,返回一个 Promise


好了,我们成功写了一个既能使用Promise 又能使用 Callback 的函数,这样,无论使用我们库的用户想要什么 Style 都能一一满足。当然,实际情况比这复杂得多,还得考虑多个参数等的情况,否则 中也不会有一堆与 Node-style 交互的函数了。


上面是对原生 API 封装的情况,此外,越来越多常用的三方库都支持直接返回一个 Promise 对象,例如 ,这时,如果我们要包装一个同时支持两者的 API 就变得简单了。我们可以利用 Promise 的链式特性,直接在 Promise 的结尾添加相关逻辑,而无需在中间步骤中反复调用 callback(null,data) 或者 callback(err, null)(这不仅仅是麻烦的问题,还会因为逻辑不严谨导致 callback 调用多次的问题,你看,这又是 Promise 的优点,降低你犯错的概率)。



让我们尝试添加 Callback 支持。



So easy, 不但这样,而且我们可以很容易抽象一个函数,对于那些非可变参数的 Promise 工厂函数添加 Callback 返回。实际上,有很多库都写了这样一个函数,我在 NPM 上搜了一圈,找到了一个下载量特别大的,肯定靠谱,,啧啧。


让我们测试一下:


完美通过,从此,Promise Callback 手牵手肩并肩,过上了幸福的二人世界。


Happy Ending.

...
...
...

然而,有那么一天,我们不小心在用divide3 的时候,手一抖,写错了个字。


你没有看错,什么都没有,编程中最怕的不是报错,而是不报错,如果在你庞大的代码块中有这么一个地方,默默地出现了异常,又默默地消失,不留痕迹,这样太恐怖了。


这一切都是为什么,相信你也猜到了,因为Promise


来看看 的源代码。(让我想到了事件)



那我们的异常是从在哪儿被吞没的呢?


相信大家都明白了原因,再看看这个模块的,不得不为这些用户担忧啊╯◇╰


知道了原因,让我们试着改一下,就用前面所说的使用 setTimeout Promise 链的结尾异步抛错。


终于成功发现了 consale 的拼写错误,妈妈再也不担心我们出现 typo 了。


Tips

能够兼容 Promise Callback 确实是件很棒的事情,用第三方代码前请尽量理解其原理,短小的话完全可以自己写一个。Promise虽好,可不要乱用哦,实时牢记它会吞没错误的风险。


另外,上面那种实现也是有问题的,仔细看你就会发现,它会使得错误栈多了一层。更好的方法如下:

最后

希望你看完之后能够继续喜爱并使用 Promise,如果我遇到过的问题能够帮助你的话,那就更好了,Good Luck


关于本文

作者:@

原文链接:http://www.jianshu.com/p/9e4026614fbe



您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存