【JS红宝书第四版读书笔记】 第11章 期约

78 阅读10分钟

一、      异步编程

1)       同步行为:内存中顺序执行的处理器指令,每条指令执行后能够立刻获得存储在本地的信息;

2)       异步行为:类似于系统中断,当前进程外部实体可以触发代码执行;例如定时回调,由系统定时器触发,时间一到,会产生一个入队执行的中断,将回调任务放入消息队列中,而该任务何时出队执行JS运行时无法获知(但一定是在当前线程的同步代码之后);

3)       以往的异步编程模式——回调模型:早期Js通过定义回调函数来表明异步操作完成,如定时器到时之后回调任务被推入消息队列,何时执行仍然不可见。串联多个异步操作需要深度嵌套回调函数(回调地狱);

a)       异步返回值:异步操作的返回值常常通过给回调函数传参、在回调函数中使用参数来反映;

b)       失败处理:try-catch实现;必须在初始化异步操作时定义回调,已经不可取;

c)        嵌套异步回调:如果异步返回值依赖另一个异步,就需要嵌套回调,使得代码复杂难以维护;

二、      期约

1.     Promises/A+规范

ES6实现了基于CommonJS的Promises/A规范制定的Promises/A+规范,即Promise类型,已成为主导性异步编程机制,所有现代浏览器都支持ES6期约。

2.     期约基础

创建期约实例需要使用new操作符,并传入执行器(executor)函数作为参数;

1)       期约状态机

image.png

a)       期约是有状态的对象,共三种状态,待定状态下期约可以落定(settled)兑现拒绝;期约一旦落定,状态不能再改变,也不能保证期约一定能脱离待定状态;

b)       期约的状态是私有的,外部无法修改:期约故意将异步代码封装起来,与外部同步代码隔离开;

1)       解决值、拒绝理由

a)       期约主要有两种用途:一是表示一个异步操作,期约的状态表示操作是否完成,“待定”表示尚未开始或正在执行,“兑现”和“拒绝”表示成功与否;

b)       二是程序期待当期约落定后能够访问异步操作的返回值,对应“兑现”和“拒绝”两种状态,期约会返回“值”(value)或拒绝“理由”(reason);二者都是期约内部私有的,是包含原始值或对象的不可修改的引用;可选;默认值为undefined;期约落定后,异步代码会始终收到“值”或拒绝“理由”;

2)       通过执行函数控制期约状态

a)       由于期约的状态是私有的,所以只能在内部操作,通过执行器函数完成,执行器函数有两大职责:初始化期约和改变期约状态;

b)       执行器函数通过两个函数参数来改变期约状态,通常命名为resolve()和reject();

c)        执行器函数代码是同步执行的;

d)       状态被改变后,再使用resolve()和reject()会静默失败;

3)       Promise.resolve()

a)       可以实例化一个初始状态为解决的期约,该期约实例值为传给函数的第一个参数;

b)       对于非期约值,该函数会将其转换成已解决的期约实例,包括错误对象也会变成已解决状态的期约;只有抛出异常会转为拒绝状态的期约;

c)        对于参数是期约,该函数类似于一个空包装,会保留参数的状态,并且是幂等的,无论嵌套多少层都没有区别;

4)       Promise.reject()

a)       实例化一个拒绝状态的期约并抛出异步错误(异步错误无法通过try-catch捕获,只能通过错误处理程序捕获),拒绝理由为传给函数的第一个参数;该参数也会继续传给处理程序;

b)       不是幂等的,若参数是期约,只会将之变成它返回的拒绝期约的理由;

1.     期约的实例方法

1)       Promise.prototype.then()

a)       该方法用于为期约实例添加处理程序,最多接收两个参数:onResolved()和onRejected()处理程序,分别对应“解决”和“拒绝”两种状态的后续处理;当期约落定为某种状态后,就会执行处理程序;

b)       会静默忽略传入的非函数参数,且不传onResolved()只传onRejected()时要使用undefined占位;

c)        返回值是一个新的期约实例,该新期约实例基于onResolved()或onRejected()处理程序的返回值,通过Promise.resolve()方法包装:

                     i.            如果没有提供处理程序,则Promise.resolve()包装原期约;

                    ii.            如果处理程序中没有显式地返回值,则Promise.resolve()包装默认值undefined;

2)       Promise.prototype.catch():用于为期约添加onRejected()处理程序,相当于Promise.prototype.then(null,onRejected)的语法糖;

3)       Promise.prototype.finally()

a)       用于为期约添加onFinally()处理程序,该程序在两种落定状态时都会执行,主要为了避免两种处理程序中出现代码冗余,但该程序无法得知期约最终会落定为哪种状态,所以主要用来添加清理代码;

b)       返回一个期约实例,因为onFinally()被设计为期约无关的方法,所以返回的期约实例大多是父期约的传递(处理程序返回待定期约则方法返回待定期约,但期约一旦落定,新期约仍然会原样后传初始期约;处理程序返回拒绝期约或抛出异常则返回拒绝期约)

4)       非重入期约方法:当期约落定时,期约的处理程序仅仅只是进入消息队列中排期执行,跟在添加处理程序之后的同步代码一定会比处理程序先执行,被称为非重入特性;then()、catch()、finally()都具有这一特性;

5)       邻近处理程序的执行顺序:如果给期约添加了多个处理程序,按照添加顺序依次执行

6)       解决值和拒绝理由的传递:解决值和拒绝理由将作为执行函数的第一个参数向后传递,当期约落定后,处理程序会根据收到的解决值或拒绝理由进行操作;Promise.resolve()和Promise.reject()在调用时就会接收解决值或拒绝理由,它们返回的期约也同样会将这些值传给处理程序;

7)       拒绝期约和拒绝错误处理

a)       期约的执行函数或处理程序中出现错误会导致拒绝,错误对象会成为拒绝理由;

b)       所有错误都是异步抛出,必须通过异步的onRejected()处理程序捕获(不包括执行函数中的错误);异步抛出的错误不会阻止后面的同步代码继续执行(对比同步错误后面的代码会停止执行)

c)        执行函数中的错误在期约落定前仍然可以通过try/catch捕获

 

2.     期约连锁与期约合成

A.       期约连锁

a)       期约连锁:期约实例方法会返回新的期约,因此可以连缀调用方法就能够串行化异步任务,解决回调地狱;

b)       期约图:期约连锁可以构建有向非循环图——每个期约都是图中一个节点,处理程序则是有向边,图的方向就是期约的落定顺序;

B.       期约合成

a)       Promise.all()

                     i.            Promise.all()接收一个可迭代对象(通过Promise.resolve()转化为期约),返回一个新期约;

                    ii.            返回的期约会在一组期约全部解决后才落定为解决期约,解决值是所有解决值按照迭代器顺序构成的数组;

                  iii.            若存在一个期约待定则返回待定期约,若存在一个期约拒绝则返回拒绝期约且拒绝理由为第一个拒绝的期约的拒绝理由;

b)       Promise.race():接受一个可迭代对象,返回一个包装期约——是一组中最先落定的期约的镜像;

c)        串行期约合成:可以使用任意多个函数作为处理程序,合成一个连续传值的期约连锁;

image.png

一、      异步函数

1.     异步函数

1)       async

a)       用于声明异步函数,代码仍然同步执行(不含await的),使用return关键字返回的值会被Promise.resolve()包装成期约对象;异步函数始终返回期约对象。

b)       异步函数中抛出异常会返回拒绝期约;

c)        拒绝期约Promise.reject()的错误不会被异步函数捕获;

2)       await

a)       await关键字可以暂停执行异步函数后面的代码,让出JS运行时的执行线程,等待期约解决,再恢复异步函数的执行;

b)       await期待实现了thenable接口的对象,这样可以由await来“解包”(执行相应的处理);如果是常规值就被当作已经解决的期约;如果是抛出异常则会返回拒绝的期约;如果是拒绝期约,如前所述,异步函数无法捕获,但会释放错误值(返回拒绝期约,然后可以通过错误处理程序处理);

c)        await必须用在异步函数中,不能在顶级上下文如

2.     停止与恢复执行

1)       JS运行时碰到await关键字之前异步函数正常执行,之后会记录在哪里暂停执行,等到await右边的值可用时,JS向消息队列中推送一个任务,添加完任务退出异步函数,该任务出列时恢复异步函数的执行;

2)       await后面等待的是期约时,遇到await异步函数暂停,向消息队列中添加期约落定后执行的任务(处理程序),期约落定,再将给await提供值的任务添加到消息队列,异步函数退出。解决期约的处理程序出队时,获得要提供给await的值。将恢复执行异步函数的任务入队;

image.png

3.     异步函数策略

1)       异步函数实现sleep()

image.png

image.png

2)       利用平行执行

a)       如果有多个await等待多个期约时,异步函数会依次暂停,这样能够保证执行顺序,但总的执行时间会更长; image.png

image.png

b)       而不保证顺序时,可以先一次性初始化所有期约,再使用await分别等待结果,这样等待期约落定的时间有一定的复用

image.png

image.png

上面两个例子主要为了说明时间的复用,所以把每个期约的超时设置为相同时间,没有说明执行顺序问题,但实际情况中每个期约用时不同,一次性初始化期约再使用await等待将不能保证期约顺序(虽然期约没有按顺序,但await按顺序收到了期约值),例子如下:

image.png

image.png

image.png

3)       串行执行期约:串行执行期约原本需要连缀使用期约的实例方法,有了await之后,await也能够传递返回值,串行执行期约变得更简单;

4)       栈追踪与内存管理:期约和异步函数功能有所重叠,但在内存中差异很大,JS引擎在创建期约时会尽可能保留完整的调用栈,抛出错误时,错误处理逻辑可以获取到调用栈,也会出现在栈追踪信息中;而使用异步函数来等待期约时,错误信息中不会出现多余的函数调用信息,不会带来额外的消耗。