Promise如何重塑异步编程?

38 阅读7分钟

异步

回调函数的问题

第一,大脑对于事情的计划方式是线性的、阻塞的、单线程的语义,但是回调表达异步流 程的方式是非线性的、非顺序的,这使得正确推导这样的代码难度很大。难于理解的代码 是坏代码,会导致坏 bug。 我们需要一种更同步、更顺序、更阻塞的的方式来表达异步,就像我们的大脑一样。 第二,也是更重要的一点,回调会受到控制反转的影响,因为回调暗中把控制权交给第三 方(通常是不受你控制的第三方工具!)来调用你代码中的 continuation。这种控制转移导 致一系列麻烦的信任问题,比如回调被调用的次数是否会超出预期。 可以发明一些特定逻辑来解决这些信任问题,但是其难度高于应有的水平,可能会产生更 笨重、更难维护的代码,并且缺少足够的保护,其中的损害要直到你受到 bug 的影响才会 被发现。

promise

我们确定了通过回调表达程序异步和管理并发的两个主要缺陷:**缺乏顺序性 和可信任性。**既然已经对问题有了充分的理解,那么现在是时候把注意力转向可以解决这 些问题的模式了。

控制反转问题

回调函数的控制反转:回调函数来封装程序中的 continuation,然后把回调交给第三方(甚至可 能是外部代码),接着期待其能够调用回调,实现正确的功能。

想象一下,不把自己程序的 continuation 传给第三方,而是希望第三方给我们提供了解其任务何时结束的能力,然后由 我们自己的代码来决定下一步做什么

这种范式就称为 Promise。

这里因为我学习过 promise 这个东西很难概括,所以还是不长篇大论的写 promise 是什么了

thenable

因此,识别 Promise(或者行为类似于 Promise 的东西)就是定义某种称为 thenable 的东 西,将其定义为任何具有 then(..) 方法的对象和函数。我们认为,任何这样的值就是 Promise 一致的 thenable。

Promise 信任问题

Promise 模式构建的可能最重要的特性:信任。

先回顾一下只用回调编码的信任问题。把一个回调传入工具 foo(..) 时可能出现如下问题:

  • 调用回调过早;
  • 调用回调过晚(或不被调用);
  • 调用回调次数过少或过多;
  • 未能传递所需的环境和参数;
  • 吞掉可能出现的错误和异常。
调用过早

会导致竞态条件。一个任务有时同步完成,有时异步完成.

promise 解法:Promise 就不必担心这种问题,因为即使是立即完成的 Promise(类似于 new Promise(function(resolve){ resolve(42); }))也无法被同步观察到。

人话说,promise 就算已经决议了,他也是事件循环推到

调用过晚

一个 Promise 决议后,这个 Promise 上所有的通过 then(..) 注册的回调都会在下一个异步时机点上依次被立即调用。这些回调中的任意一个都无法影响或延误对其他回调的调用。

也就是时间循环一个一个任务来

回调未调用

Promise 可以通过几种途径解决。

首先,没有任何东西(甚至 JavaScript 错误)能阻止 Promise 向你通知它的决议(如果它 决议了的话)。如果你对一个 Promise 注册了一个完成回调和一个拒绝回调,那么 Promise 在决议时总是会调用其中的一个。

当然,如果你的回调函数本身包含 JavaScript 错误,那可能就会看不到你期望的结果,但 实际上回调还是被调用了。后面我们会介绍如何在回调出错时得到通知,因为就连这些错 误也不会被吞掉。

如果 Promise 本身永远不被决议呢?Promise 也提供了解决方案,竞态(使用 race(只要最先决议的 promise 的返回值),配合定时器就能保证一定会调用回调)

// 用于超时一个Promise的工具

function timeoutPromise(delay) {

return new Promise(function (resolve, reject) {

setTimeout(function () {

reject("Timeout!");

}, delay);

});

}

// 设置foo()超时

Promise.race([

foo(), // 试着开始foo()

timeoutPromise(3000) // 给它3秒钟

])

.then(

function () {

// foo(..)及时完成!

}, function (err) {

// 或者foo()被拒绝,或者只是没能按时完成

console.log(err)

}

);
调用次数过少或过多

“过少”的情况就是调用 0 次,上面解释过 的“未被”调用是同一种情况。

“过多”的情况很容易解释。Promise 的定义方式使得它只能被决议一次。所以回调就只会调用一次

未能传递参数/环境值

Promise 至多只能有一个决议值(完成或拒绝)。 如果你没有用任何值显式决议,那么这个值就是 undefined,这是 JavaScript 常见的处理方 式。 但不管这个值是什么,无论当前或未来,它都会被传给所有注册的(且适当的完成或 拒绝)回调。

注:如果使用多个参数调用 resovle(..)或者 reject(..),第一个参数之 后的所有参数都会被默默忽略。

如果要传递多个值,你就必须要把它们封装在单个值中传递,比如通过一个数组或对象。

promise 是否真的能信任

你肯定已经注意到 Promise 并没有完全摆脱回调。它们只是改变了传递回调的位置。我 们并不是把回调传递给 foo(..),而是从 foo(..)得到某个东西(外观上看是一个真正的 Promise),然后把回调传给这个东西。

但是,为什么这就比单纯使用回调更值得信任呢?

关于 Promise 的很重要但是常常被忽略的一个细节是,Promise 对这个问题已经有一个解决 方案。包含在原生 ES6 Promise 实现中的解决方案就是 Promise.resolve(..)。

如果向 Promise.resolve(..) 传递一个非 Promise、非 thenable 的立即值,就会得到一个用 这个值填充的 promise。

更重要的是,如果向 Promise.resolve(..)传递了一个非 Promise 的 thenable 值,前者就会 试图展开这个值,而且展开过程会持续到提取出一个具体的非类 Promise 的最终值。

假设我们要调用一个工具 foo(..),且并不确定得到的返回值是否是一个可信任的行为良 好的 Promise,但我们可以知道它至少是一个 thenable。

// 而要这么做:
Promise.resolve( foo( 42 ) ) .then( function(v){ console.log( v ); } );
总结

Promise 链不仅是一个表达多步异步序列的流程控制,还是一个从一个步 Promise | 201 骤到下一个步骤传递消息的消息通道。

默认拒绝: 如果你调用 promise 的 then(..),并且只传入一个完成处理函数,一个默认拒绝处理函数 就会顶替上来 默认拒绝处理函数只是把错误重新抛出,这最终会使得 p2(链接的 promise) 用同样的错误理由拒绝。从本质上说,这使得错误可以继续沿着 Promise 链传播下去,直 到遇到显式定义的拒绝处理函数。

var p = new Promise( function(resolve,reject){ reject( "Oops" ); } );
var p2 = p.then( function fulfilled(){ // 永远不会达到这里 }
// 假定的拒绝处理函数,如果省略或者传入任何非函数值
// function(err) { // throw err; // } );

默认完成: 默认的完成处理函数只是把接收到的任何传入值传递给下一个步骤 (Promise)而已。

var p = Promise.resolve( 42 );
p.then( // 假设的完成处理函数,如果省略或者传入任何非函数值
// function(v) {
// return v;
// }
null,
function rejected(err){ // 永远不会到达这里 } );

让我们来简单总结一下使链式流程控制可行的 Promise 固有特性。 • 调用 Promise 的 then(..) 会自动创建一个新的 Promise 从调用返回。 • 在完成或拒绝处理函数内部,如果返回一个值或抛出一个异常,新返回的(可链接的) Promise 就相应地决议。 • 如果完成或拒绝处理函数返回一个 Promise,它将会被展开,这样一来,不管它的决议 值是什么,都会成为当前 then(..)返回的链接 Promise 的决议值。