Promise

Promise基础

1、 通过执行函数控制期约状态

new Promise(() => setTimeout(console.log, 0, 'executor'));
setTimeout(console.log, 0, 'promise initialized');
// executor
// promise initialized

let p = new Promise((resolve, reject) => setTimeout(resolve, 1000));
setTimeout(console.log, 0, p); 
// 在console.log打印期约实例的时候,还不会执行超时回调(即resolve()) // Promise <pending>

2、为避免期约卡在待定状态,可以添加一个定时退出功能。比如,可以通过 setTimeout 设置一个 7 10 秒钟后无论如何都会拒绝期约的回调:

let p = new Promise((resolve, reject) => { setTimeout(reject,10000); //10秒后调用reject() // 执行函数的逻辑
});
setTimeout(console.log, 0, p);      // Promise <pending>
setTimeout(console.log,11000,p); //11秒后再检查状态 // (After 10 seconds) Uncaught error
// (After 11 seconds) Promise <rejected>

3、 Promise.resolve() 期约并非一开始就必须处于待定状态,然后通过执行器函数才能转换为落定状态。通过调用 Promise.resolve()静态方法,可以实例化一个解决的期约。下面两个期约实例实际上是一样的:

let p1 = new Promise((resolve, reject) => resolve()); 
let p2 = Promise.resolve();

对这个静态方法而言,如果传入的参数本身是一个期约,那它的行为就类似于一个空包装。因此, Promise.resolve()可以说是一个幂等方法,如下所示:

这个幂等性会保留传入期约的状态:

4、 Promise.reject() 与 Promise.resolve()类似,Promise.reject()会实例化一个拒绝的期约并抛出一个异步错误 (这个错误不能通过 try/catch 捕获,而只能通过拒绝处理程序捕获)。下面的两个期约实例实际上是 一样的:

这个拒绝的期约的理由就是传给 Promise.reject()的第一个参数。这个参数也会传给后续的拒 绝处理程序:

关键在于,Promise.reject()并没有照搬 Promise.resolve()的幂等逻辑。如果给它传一个期 约对象,则这个期约会成为它返回的拒绝期约的理由:

5、 同步/异步执行的二元性 Promise 的设计很大程度上会导致一种完全不同于 JavaScript 的计算模式。下面的例子完美地展示 了这一点,其中包含了两种模式下抛出错误的情形:

第一个 try/catch 抛出并捕获了错误,第二个 try/catch 抛出错误却没有捕获到。乍一看这可能 有点违反直觉,因为代码中确实是同步创建了一个拒绝的期约实例,而这个实例也抛出了包含拒绝理由 的错误。这里的同步代码之所以没有捕获期约抛出的错误,是因为它没有通过异步模式捕获错误。从这 里就可以看出期约真正的异步特性:它们是同步对象(在同步执行模式中使用),但也是异步执行模式 的媒介。 在前面的例子中,拒绝期约的错误并没有抛到执行同步代码的线程里,而是通过浏览器异步消息队 列来处理的。因此,try/catch 块并不能捕获该错误。代码一旦开始以异步模式执行,则唯一与之交互 的方式就是使用异步结构——更具体地说,就是期约的方法。

Promise实例方法

Promise.prototype.then()

如前所述,两个处理程序参数都是可选的。而且,传给 then()的任何非函数类型的参数都会被静 默忽略。如果想只提供 onRejected 参数,那就要在 onResolved 参数的位置上传入 undefined。这 样有助于避免在内存中创建多余的对象,对期待函数参数的类型系统也是一个交代。

这个新期约实例基于 onResovled 处理程序的返回值构建。换句话说,该处理程序的返回值会通过 Promise.resolve()包装来生成新期约。如果没有提供这个处理程序,则 Promise.resolve()就会 包装上一个期约解决之后的值。如果没有显式的返回语句,则 Promise.resolve()会包装默认的返回 值 undefined。

抛出异常会返回拒绝的期约:

注意,返回错误值不会触发上面的拒绝行为,而会把错误对象包装在一个解决的期约中:

Promise.reject()替代之前例子中的 Promise.resolve()是一样的道理

Promise.prototype.catch()

Promise.prototype.catch()方法用于给期约添加拒绝处理程序。这个方法只接收一个参数: onRejected 处理程序。事实上,这个方法就是一个语法糖,调用它就相当于调用 Promise.prototype. then(null, onRejected)。

下面的代码展示了这两种同样的情况:

Promise.prototype.catch()返回一个新的期约实例:

拒绝期约与拒绝错误处理

这个例子同样揭示了异步错误有意思的副作用。正常情况下,在通过 throw()关键字抛出错误时, JavaScript 运行时的错误处理机制会停止执行抛出错误之后的任何指令: 所有错误都是异步抛出且未处理的,通过错误对象捕获的栈追踪信息展示了错误发生的路径。注意 错误的顺序:Promise.resolve().then()的错误最后才出现,这是因为它需要在运行时消息队列中 添加处理程序;也就是说,在最终抛出未捕获错误之前它还会创建另一个期约。

但是,在期约中抛出错误时,因为错误实际上是从消息队列中异步抛出的,所以并不会阻止运行时 继续执行同步指令:

如本章前面的 Promise.reject()示例所示,异步错误只能通过异步的 onRejected 处理程序 捕获:

串行期约合成

期约取消

期约进度通知

异步函数策略

实现 sleep()

利用平行执行

用一个 for 循环重写,就是:

就算这些期约之间没有依赖,异步函数也会依次暂停,等待每个超时完成。这样可以保证执行顺序, 7 但总执行时间会变长。 如果顺序不是必需保证的,那么可以先一次性初始化所有期约,然后再分别等待它们的结果。比如:

用数组和 for 循环再包装一下就是:

注意,虽然期约没有按照顺序执行,但 await 按顺序收到了每个期约的值:

平行控制并发数

串行执行期约

这里,await 直接传递了每个函数的返回值,结果通过迭代产生。当然,这个例子并没有使用期约, 如果要使用期约,则可以把所有函数都改成异步函数。这样它们就都返回期约了:

栈追踪与内存管理

期约与异步函数的功能有相当程度的重叠,但它们在内存中的表示则差别很大。看看下面的例子, 它展示了拒绝期约的栈追踪信息:

根据对期约的不同理解程度,以上栈追踪信息可能会让某些读者不解。栈追踪信息应该相当直接地 表现 JavaScript 引擎当前栈内存中函数调用之间的嵌套关系。在超时处理程序执行时和拒绝期约时,我 们看到的错误信息包含嵌套函数的标识符,那是被调用以创建最初期约实例的函数。可是,我们知道这 些函数已经返回了,因此栈追踪信息中不应该看到它们。 答案很简单,这是因为 JavaScript 引擎会在创建期约时尽可能保留完整的调用栈。在抛出错误时, 调用栈可以由运行时的错误处理逻辑获取,因而就会出现在栈追踪信息中。当然,这意味着栈追踪信息 会占用内存,从而带来一些计算和存储成本。 如果在前面的例子中使用的是异步函数,那又会怎样呢?比如:

这样一改,栈追踪信息就准确地反映了当前的调用栈。fooPromiseExecutor()已经返回,所以 它不在错误信息中。但 foo()此时被挂起了,并没有退出。JavaScript 运行时可以简单地在嵌套函数中 存储指向包含函数的指针,就跟对待同步函数调用栈一样。这个指针实际上存储在内存中,可用于在出 错时生成栈追踪信息。这样就不会像之前的例子那样带来额外的消耗,因此在重视性能的应用中是可以 优先考虑的。

使用 ReadableStream 主体

ReadableStream 暴露了 getReader()方法,用于产生 ReadableStream- DefaultReader,这个读取器可以用于在数据到达时异步获取数据块。数据流的格式是 Uint8Array。

异步函数非常适合这样的 fetch()操作。可以通过使用 async/await 将上面的递归调用打平:

另外,read()方法也可以真接封装到 Iterable 接口中。因此就可以在 for-await-of 循环中方 便地实现这种转换:

通过将异步逻辑包装到一个生成器函数中,还可以进一步简化代码。而且,这个实现通过支持只读 取部分流也变得更稳健。如果流因为耗尽或错误而终止,读取器会释放锁,以允许不同的流读取器继续 操作:

异步迭代

创建并使用异步迭代器

要理解异步迭代器,最简单的办法是用它跟同步迭代器进行比较。下面代码中创建了一个简单的 Emitter 类,该类包含一个同步生成器函数,该函数会产生一个同步迭代器,同步迭代器输出 0~4:

这个例子之所以可以运行起来,主要是因为迭代器可以立即产生下一个值。假如你不想在确定下一 个产生的值时阻塞主线程执行,也可以定义异步迭代器函数,让它产生期约包装的值。 为此,要使用迭代器和生成器的异步版本。ECMAScript 2018 为此定义了 Symbol.asyncIterator, 以便定义和调用输出期约的生成器函数。同时,这一版规范还为异步迭代器增加了 for-await-of 循环, 用于使用异步迭代器。 相应地,前面的例子可以扩展为同时支持同步和异步迭代:

为了加深理解,可以把前面例子中的同步生成器传给 for-await-of 循环:

虽然这里迭代的是同步生成器产生的原始值,但 for-await-of 循环仍像它们被包装在期约中一 样处理它们。这说明 for-await-of 循环可以流畅地处理同步和异步可迭代对象。但是常规 for 循环 就不能处理异步迭代器了:

关于异步迭代器,要理解的非常重要的一个概念是Symbol.asyncIterator符号不会改变生成器 函数的行为或者消费生成器的方式。注意在前面的例子中,生成器函数加上了 async 修饰符成为异步 函数,又加上了星号成为生成器函数。Symbol.asyncIterator 在这里只起一个提示的作用,告诉将 来消费这个迭代器的外部结构如 for-await-of 循环,这个迭代器会返回期约对象的序列。

处理异步迭代器的reject()

因为异步迭代器使用期约来包装返回值,所以必须考虑某个期约被拒绝的情况。由于异步迭代会按 顺序完成,而在循环中跳过被拒绝的期间是不合理的。因此,被拒绝的期约会强制退出迭代器:

使用next()手动异步迭代

for-await-of 循环提供了两个有用的特性:一是利用异步迭代器队列保证按顺序执行,二是隐藏 异步迭代器的期约。不过,使用这个循环会隐藏很多底层行为。 因为异步迭代器仍遵守迭代器协议,所以可以使用 next()逐个遍历异步可迭代对象。如前所述, next()返回的值会包含一个期约,该期约可解决为{ value, done }这样的迭代结果。这意味着必须 使用期约 API 获取方法,同时也意味着可以不使用异步迭代器队列。

参考资料

Last updated