你不知道的js(中卷)第8章 Promise

上一章讲到,用回调来实现异步的两大问题:代码缺乏顺序性;控制权交出,缺乏可信任性。

先说可信任性:传递回调的代码,是把控制权交给第三方,因而难以信任。
假如让第三方告诉我们其任务何时结束,然后由我们自己决定下一步操作呢?
这种范式就叫做Promise。
实际上,绝大多数JavaScript/DOM平台新增的异步API都是基于Promise的。

1.什么是Promise

在展示 Promise 代码之前,先从概念上完整地解释 Promise 到底是什么。

1.1 未来值

可以不关心一个值现在还是将来会得到,用一个占位符来表示它,这个占位符使得这个值不再依赖时间,我们可以在还没拿到值(未来才能拿到,当然也可能拿失败)的时候就编写整个事情的代码。

①现在值和将来值
      不管一个值是现在就能拿到,还是将来才能拿到,我们可以用同样的方式去对待它,不管它是不是异步,都可以不关心,都不影响代码逻辑和写法。
②Promise值
      作者举了个Promise的使用例子(用Promise来写“计算未来值x和未来值y的和”),并讲解了一下例子中语句的含义和特性。
      从外部看,由于Promise封装了依赖于时间的状态——等待底层值的完成或拒绝,所以Promise本身是与时间无关的。因此,Promise可以按照可预测的方式组成(组合),而不用关心时序或底层的结果。
      另外,一旦 Promise 决议,它就永远保持在这个状态,可以根据需求多次查看。
      Promise决议后就是外部不可变的值(immutable value),我们可以安全地把这个值传递给第三方,并确信它不会被有意无意地修改。
      Promise是一种封装和组合未来值的易于复用的机制。

1.2 完成事件

      Promise除了可以看作是表示“未来值”的东西,也可以用来表示一种在异步任务中的多个步骤的流程控制机制,它控制多个异步步骤的时序。
      假如有个异步步骤foo,我们不关心其细节,只想在该步骤结束以后得到通知,并做一些处理。
      典型的写法是给foo传入回调方法,通知就是foo调用的回调。
      而使用Promise的话,Promise侦听foo的事件(成功事件和失败事件),侦听到事件以后,就算得到了通知,然后再根据通知做一些处理。
      回调是对控制关系的反转,Promise这种范式则是控制关系的反转的反转。
      这样做的好处是,foo不用关心要调哪些回调,只用把结果告诉Promise,关心任务结果的人自然会去监听Promise的。这样就实现了关注点的分离。
      从本质上说,Promise对象就是分离的关注点之间一个中立的第三方协商机制。



2.具有then方法的鸭子类型

      如何确定一个值是Promise?
      instanceof Promise是不够用的,因为Promise值可能是从其它iframe拿到的,也有可能是其它库或框架自己实现的Promise,而不是用原生ES6实现的(在不支持ES6的浏览器中就可能会有)。这些情况都没法用instanceof Promise来判断。

      我们根据一个值具有哪些属性来对这个值的类型做出一些假定。这种类型检查一般称为鸭子类型(duck typing)——“如果它看起来像只鸭子,叫起来像只鸭子,那它一定就是只鸭子。”
      所以对一个值是否是Promise的检测,就会是类似这样:判读它 是一个对象 且 有then属性 且 then属性是一个function
      但这样的判断是有风险的,比如很多类库提供了具有then方法的对象/委托,就会被鸭子类型判断为Promise。这些类库要么不得不把自己的方法重新命名了以避免冲突,要么被打为“与含有Promise的代码不兼容”。
      如果有其它代码无意或恶意地给Object.prototype、Array.prototype或者其它原生原型添加了then方法,也会造成灾难。
      不过鸭子类型有时候还是有用的,只是要小心鸭子类型把不是Promise的值误判为Promise的情况。



3.Promise信任问题

      前面说到,Promise在异步代码中做的事情,一是作为“未来值”;二是反控制反转,接收“完成事件”。
      但是我们还没讨论,Promise怎么应对上一章讲到的“回调模式中存在的信任问题”。
      先回顾下回调中要面对的信任问题:回调调太早、回调调太晚或者根本不回调、回调次数过多或过少、没传递该传入的环境和参数、吞掉应该报出的错误和异常。

3.1 调用过早

      如果回调函数可能被同步地调用,而使用者却没预料到,就可能出现竞态条件,搞出bug。
      但Promise的定义,规定了即使是已经决议的Promise,也无法被同步观察到。也就是说,对一个Promise调用then(..)的时候,即使这个Promise已经决议,提供给then(..)的回调也总会被异步调用。

3.2 调用过晚

      Promise作为第三方中立的协商机制,会在 其创建对象 调用resolve或reject的时候,调度Promise对象上注册的回调函数。所以这个层面上讲,不用担心异步操作结束后对方漏了哪个回调,因为回调这件事交给Promise去做了,我们可以信任它。

      另外,也不用担心同一个Promise对象上注册的多个回调函数之间会互相延误或阻止。一个Promise决议后,这个Promise上注册的回调都会在下一个异步时机点上依次被立即调用,中间哪个回调要执行异步操作,或者要抛异常,都不会延误后面的回调被调用。

3.3 回调未调用

      如果你对一个Promise注册了一个完成回调和一个拒绝回调,那么Promise在决议时总是会调用其中的一个。
      那如果Promise本身永远不被决议呢?
      可以利用Promise提供的race,Promise.race([p1, p2])返回一个新的Promise对象,p1和p2任意一个决议,新Promise对象就决议。我们可以设置p2的内容为:设一个定时器,定时器到期后调用reject。这样,p1如果一直不决议,Promise.race([p1, p2])也会在p2的定时器到期后决议。

3.4 调用次数过多或过少

      “过少”已经在前面讲过可利用Promise.race防范。
      “过多”,由于Promise的机制,Promise对象只能决议一次,不可以既调用resolve又调用reject,也不能多次调用,Promise对象只会接受第一次决议,后续的调用都会被忽略。

3.5 未能传递参数/环境值

      Promise的机制:调用resolve或reject时传入的参数(只接收第一个参数,多的会被忽略)就会成为决议值,不传的话,决议值就是undefined。

3.6 吞掉错误或异常

      我们已经知道,直接调用reject可以使Promise决议到“拒绝”态。
      但如果Promise在创建或在获得结果的过程中出现了JavaScript异常,这个异常就会被捕获,并使Promise对象决议到“拒绝”。

例:

let test = new Promise((resolve, reject) => {let a = b}) // b是未声明的变量
test // Promise {<rejected>: ReferenceError: b is not defined

      Promise的这个细节,避免了“任务出错引起同步响应,不出错则会是异步的”的风险,使我们不会遇到料想不到的竞态条件。
      Promise甚至把JavaScript异常也变成了异步行为。
      凡是Promise的决议都会异步执行。

3.7 是可信任的Promise吗

      Promise并没有完全摆脱回调,只是我们之前是直接把回调丢给异步任务,现在则是把回调丢给 根据异步任务得到的一个东西。
      为什么这样比回调更可信?怎么确定返回的东西就是个可信任的Promise呢?

      在原生ES6 Promise实现中的解决方案就是Promise.resolve。
      Promise.resolve(一个非Promise、非thenable的立即值) → 就会得到一个已经决议到“完成”的Promise对象。

      Promise.resolve(一个Promise值) → 就会得到里面那个Promise对象(新的Promise会替代掉原来那个,等它决议才能得到结果。)

      Promise.resolve(一个thenable值,它不是真的Promise,但有then方法) → Promise就会试图展开这个thenable,直到最后提取出一个非thenable的最终值。

      所谓的展开:
      Promise调用这个thenable的then方法,给它传入Promise自己的完成回调和失败回调。
      如果它调失败回调,那就决议到“拒绝”
      如果它调成功回调,那就决议到“完成”。
      如果它调成功回调,并且还传参传了个新的thenable,那就继续调新的thenable的then方法。


展开.PNG

      假如我们要调一个工具foo(..),但不确定得到的返回值是不是一个可信任的规范的promise,我们可以把它丢给Promise.resolve,Promise.resolve会替你过滤掉thenable值。
      从语义上来讲,Promise不会直接把一个thenable当作决议值丢出去,而是把它看作一个“未来值”,等它决议(也就是等它调了传入的成功回调或失败回调),一直到它不会再决议出“未来值”而是决议出“普通值”,这个普通值才被视作决议结果。

      比如:foo(42).then(回调),要是不确定foo会不会返回盗版Promise,可以这样:Promise.resolve(foo(42)).then(回调)

3.8 建立信任

      前面的讨论已经讲解了为什么Promise是可信任的,以及为什么对于构建健壮可维护的应用来说,建立信任很重要。
      Promise模式通过可信任的语义把回调的控制反转 反转回来,我们把控制权放在了可信任的系统(Promise)中,这个系统设计的目的就是为了使异步编码更清晰。



4.链式流

      Promise并不只是一个单步执行this-then-that操作的机制,还可以把多个Promise连接到一起来表示一系列的异步步骤。

      Promise怎么做到串联一系列异步步骤呢?
      1.一个Promise的then方法不仅接收成功、失败回调,而且还会返回一个新的Promise,新的Promise表示then接收的回调的完成情况
      2.如果then接收的回调返回的不是一个普通值而是一个“未来值”(Promise、thenable),未来值会被等待(展开),等到出结果,这个结果才会被当作异步任务链中当前环节的执行结果,并被传递给这个“环节”的then方法所接收的回调。
      3.then链中任务如果失败(reject)或者抛出异常,那么其后续环节都会reject,当然,也可以在出错环节的后面任一环节进行捕捉,捕捉掉错误之后,这一环节就算“正常”结束,进行了捕捉的环节的后续环节不会因为前面没捕捉的错误而eject了。

      比起用回调写出来的异步代码,Promise链接异步任务的顺序表达是个很大的进步。

      最后作者从语义上分析了一下Promise相关的几个术语:
      1.为什么Promise构造器传递给被包裹的任务的两个回调函数,通常被分别称为resolve和reject呢?
      答:reject没什么说的,是失败的时候调的。resolve被叫做resolve而不叫做fllfilled,是因为resolve方法可能接收到一个未来值,未来值的展开结果可能是reject的。所以用"resolve"这个名称更恰当。
      2.then方法接收的回调呢?叫什么名字合适?
      答:因为它俩永远是分别处理任务的“完成”和“失败”,所以叫做fulfilled和rejected就很合适了。
      而且在ES6规范里,它们也被叫做onFulfilled和onRejected,实至名归。



5.错误处理

      Promise的错误处理对初接触的人来说不是很直观,一个任务决议后,其fulfilled回调执行失败,不是由注册在该任务上的rejected回调去接收错误信息,而是该任务的下一环任务上注册的rejected回调去接收。
      虽然这样其实是合理的,但是乍一看,有点令人难以理解,然后一不小心会因为理解有误,写出bug。

      有些开发者提出,在实际开发中,可以在then后面加catch,以保证then接收的回调函数如果出错了也能被后面那个catch抓到。
      但假如catch接收的错误处理方法自身出错了呢?
      这种写法还是不能保证整条链任意环节出的错都能被捕获。

      还有些办法可以判断Promise任务链中是否有错误“从始至终未被捕获”。
      如:设置定时器,一定时间内拒绝态的Promise没有被注册错误处理函数,就当它是“从始至终未被捕获”,当然,这样肯定有点问题。
      还有让浏览器对Promise变量回收时判断它是否处于拒绝态,如果是说明它“从始至终未被处理”,但这样不能兼顾到因为各种原因变量没释放的情况。

      接下来作者讲了理论上“更好的Promise”该拥有的特性,“理想中的Promise”在决议到拒绝后,要默认报告未被处理的拒绝(在决议后的下一个异步时间点),除非它被显式调用了defer,或者它已经注册了一个错误处理函数。
      (我没懂什么叫“报告”,控制台报错算“报告”吗?如果是的话现在不就已经是了吗,没catch的错误会报到控制台。可能现在我接触到的就是优化过的Promise。)


6.Promise模式

      除了Promise链这样的顺序模式,基于Promise构建的异步模式抽象还有很多变体。

Promise.all([...])

      在经典的编程术语中,门(gate)是这样一种机制:要等待两个或更多并行 / 并发的任务都完成才能继续。它们的完成顺序并不重要,但是必须都要完成,门才能打开并让流程控制继续。
      在Promise API中,这种模式被称为all([...])
      Promise.all([...])返回的主promise在且仅在所有成员promise都完成后才会完成。任一成员promise被拒绝,主promise就会被拒绝,并丢弃其它成员promise的决议结果。

Promise.race([...])

      多个任务并发运行,只响应“第一个跨过终点线的任务”,这种模式传统上称为门闩,但在Promise中称为竞态。(注意不要混淆这里的“竞态”,和相当于bug的“竞态条件”)
一旦有任何一个成员Promise决议为完成,Promise.race([...])就会完成。

      那么从行为上看,那些被丢弃的成员promise会发生什么呢?
      假如一个成员任务内部保留着一些资源,然后这个成员任务被主任务忽略了,那么是不是应该有什么api可以用于主动释放这些资源或者取消可能产生的副作用?
      (这段话看不懂,首先,“一个Promise内保留了一些资源”是什么意思,指的是Promise里边包裹的任务所用到的资源吗?这些资源会在决议之后释放吧?而且这个时候主动释放资源,是不打算让这个任务走下去了吗?)
      然后作者提到一种未来的Promise可以有的api:finally,用于接收在主promise任务决议后进行清理的回调。

all([...])和race([...])的变体

除了原生Promise提供的all和race,还有几种其它的变体。
none([...])
any([...])
first([...])
(其实我看不懂any和first的逻辑有什么区别)
有些Promise抽象库提供了这些支持,你也可以自己实现。
小练习:自己实现一下first()

并发迭代

      假如对一堆异步任务要做同样的处理,而且这些处理不是同步操作,就需要写个类似forEach(但forEach是用于同步操作的)的迭代工具方法。
      书里写了个实现的示例,不抄录了。



7.Promise API概述

      Promise构造器接收一个函数参数,这个函数表示Promise对象的内容,函数会在new Promise语句执行到的时候被立即同步调用。
      其它略。



8 Promise局限性

8.1 顺序错误处理

      作者说,一个Promise链,如果其中一个环节出了错误并且被错误处理函数捉掉了,那么在链底你就觉察不到链中其实有环节失败。类似try catch,error被catch了以后就不会再往上抛,你就不知道底下其实发生过错误。
      (我看不出来为什么这也算缺陷。。不过作者说,这算是一个限制)

8.2 单一值

      如果一个Promise需要返回多个值,只能把它做成个对象或者数组,不过也可以思考下是不是里边的逻辑应该拆分了。

8.3 单决议

      Promise适用于只决议一次的异步。像事件、数据流这样的模式,不适合直接使用Promise,起码得对Promise再包一层逻辑才能用。

8.4 惯性

      讨论了一些把原有的回调风格的代码修改成Promise风格的代码所要做的事,介绍了一些起转换作用的工具函数

8.5 无法取消的Promise

      Promise一旦创建就无法取消。如果允许Promise的取消,那么Promise的一个消费者就可以影响其它消费者查看这个Promise,这违背了未来值的可信任性(外部不变性)。
      单独的一个Promise并不是一个真正的流程控制机制,这就是为什么Promise取消总是让人感觉很别扭。相比之下,集合在一起的Promise构成的链(可以称之为一个“序列”),就是一个流程控制的表达,将取消定义在这个抽象层次上是合适的。

8.6 Promise性能

      Promise与不可信任的裸回调相比会稍慢一些。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,377评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,390评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,967评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,344评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,441评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,492评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,497评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,274评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,732评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,008评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,184评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,837评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,520评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,156评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,407评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,056评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,074评论 2 352

推荐阅读更多精彩内容