对JavaScript异步的思考与理解(回调和Promise)

140 阅读9分钟

回调

是解决异步问题最早的一种手段。本质就是在未来某一时间点,执行指定代码。 弊端:

  • 不具有顺序性。回调代码容易产生跳跃性,不符合人脑顺序性的逻辑,同时还容易造成地狱回调,加大理解难度。
  • 不可信任性。回调函数的控制权反转了,通常在第三方手里,这种控制反导致一系列麻烦的信任问题,比如我们无法控制回调的执行次数,执行时机,同时无法捕获错误。
ajax('/list', function() {
    //回调函数,在请求到数据后由第三方工具执行。
})

针对回调设计的一种改进:关注分离

ajax('/list', success, fail)

将回调拆分成 成功的回调和失败的回调,fail通常是可选的,如果没有提供的话,这个错误会被吞掉。

Promise

  1. 是对于回调产生的控制反转的一种解决方案。

  2. 这种范式规定了:如果我们不把自己程序的continuation传给第三方,而是希望第三方给我们提供了解其任务何时结束的能力,然后由我们自己的代码来决定下一步做什么。

  3. 可以从另外一个角度看待Promise的决议:一种在异步任务中作为两个或更多步骤的流程控制机制,时序上的this-then-that。

针对3的解释: 假定要调用一个函数foo(..)执行某个任务。我们不知道也不关心它的任何细节。这个函数可能立即完成任务,也可能需要一段时间才能完成。 我们只需要知道foo(..)什么时候结束,这样就可以进行下一个任务。换句话说,我们想要通过某种方式在foo(..)完成的时候得到通知,以便可以继续下一步。

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

  2. 从回调的角度理解Prmise: ==在典型的JavaScript风格中,如果需要侦听某个通知,你可能就会想到事件。因此,可以把对通知的需求重新组织为对foo(..)发出的一个完成事件(completion event,或continuation事件)的侦听。==

特性

  • 一旦Promise决议,它就永远保持在这个状态。此时它就成为了不变值(immutable value),可以根据需求多次查看。
  • 通过.then可以返回一个新的promise实例,实现链式调用。
  • API then()符合关注分离原则,支持同时传入成功和失败的回调。

理解

Promise对象就是分离的关注点之间一个中立的第三方协商机制。

也就是说Promise对象只负责对异步代码进行监听,一旦执行结束就resolve或reject;而then函数又是负责监听Promise对象,进而执行异步代码的后续部分。

它是异步代码和后续代码中间的第三方,好处就是实现了关注分离,同时又把回调函数的执行权再次反转到自身。


Promise的API

Promise.resolve
  1. 返回一个成功的Promise决议。

  2. ==如果向其传递一个非promise或非thenable的值,就会得到一个用这个值填充的promise。==

const p1 = new Promise((resolve) => {
    resolve(20)
})
const p2 = Promise.resolve(20)
p1 == p2 // 这里p1和p2等价
  1. 而如果向Promise.resolve传递一个Promise对象,就会返回同一个promise,如下:
const p1 = Promise.resolve(20)

const p2 = Promise.resolve(p1)

p1 == p2 // true

补充: 如果向Promise.resolve传递的Promise对象展开后得到一个拒绝状态,那么从Promise.resolve(..)返回的Promise实际上就是这同一个拒绝状态。 ==所以对这个API方法来说,Promise.resolve(..)是一个精确的好名字,因为它实际上的结果可能是完成或拒绝==

  1. Promise.reolve还有一个用处,可以用来将不可信任的thenable转变成可信任的Promise对象。

Promise.resolve(..)可以接受任何thenable,将其解封为它的非thenable值。从Promise. resolve(..)得到的是一个真正的Promise,是一个可以信任的值。如果你传入的已经是真正的Promise,那么你得到的就是它本身,所以通过Promise.resolve(..)过滤来获得可信任性完全没有坏处。

// p是一个不可信任的thenable值
const p = {
    then: function(cb, errcb) {
        cb(20),
        errcb('error message')
    }
}
// 也就意味着:成功和失败的回调都会执行,不可被信任
p.then((res) => {
    console.log(res) // 20
}, (err) => {
    console.log(err) // error message
})

// 解决办法如下:通过Promise.resolve将p从不可信任的thenable转换成可信任的promise实例
Promise.resolve(p)
.then((res) => {
    console.log(res) // 20
}, (err) => {
    console.log(err) // 永远不会执行到这
})

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

new Promise()构造器
  1. 构造器Promise(..)必须和new一起使用,并且必须提供一个函数回调。==这个回调是同步的或立即调用的==
  2. reject(..)就是拒绝这个promise;但resolve(..)既可能完成promise,也可能拒绝,要根据传入参数而定。如果传给resolve(..)的是一个非Promise、非thenable的立即值,这个promise就会用这个值完成。==但是,如果传给resolve(..)的是一个真正的Promise或thenable值,这个值就会被递归展开,并且(要构造的)promise将取用其最终决议值或状态。==
then(...)和catch(...)
  1. Promise决议之后,立即会调用这两个处理函数之一,但不会两个都调用,而且总是异步调用.
  2. then(..)接受一个或两个参数:第一个用于完成回调,第二个用于拒绝回调。如果两者中的任何一个被省略或者作为非函数值传入的话,就会替换为相应的默认回调。默认完成回调只是把消息传递下去,而默认拒绝回调则只是重新抛出(传播)其接收到的出错原因。
  3. catch(..)只接受一个拒绝回调作为参数,并自动替换默认完成回调。换句话说,它等价于then(null, ..)。
  4. ==如果完成或拒绝回调中抛出异常,返回的promise是被拒绝的。如果任意一个回调返回非Promise、非thenable的立即值,这个值会被用作返回promise的完成值。如果完成处理函数返回一个promise或thenable,那么这个值会被展开,并作为返回promise的决议值。==

Promise链式调用的总结

让我们来简单总结一下使链式流程控制可行的Promise固有特性。

  • 调用Promise的then(..)会自动创建一个新的Promise从调用返回。
  • 在完成或拒绝处理函数内部,如果返回一个值或抛出一个异常,新返回的(可链接的)Promise就相应地决议。
  • 如果完成或拒绝处理函数返回一个Promise,它将会被展开,这样一来,不管它的决议值是什么,都会成为当前then(..)返回的链接Promise的决议值。

一个有趣的案例(用来理解Promise链的错误处理)

默认拒绝处理函数只是把错误重新抛出,这最终会使得p2(链接的promise)用同样的错误理由拒绝。从本质上说,这使得错误可以继续沿着Promise链传播下去,直到遇到显式定义的拒绝处理函数。代码如下:

var p = new Promise( function(resolve, reject){
  reject( "Oops" );
} );

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

Promise的错误处理

默认情况下,它假定你想要Promise状态吞掉所有的错误。如果你忘了查看这个状态,这个错误就会默默地(通常是绝望地)在暗处凋零死掉。

var p = Promise.resolve( 42 );

p.then(
    function fulfilled(msg){
        // 数字没有string函数,所以会抛出错误,但此处的错误会被吞掉。
        console.log( msg.toLowerCase() );
    },
    function rejected(err){
        // 永远不会到达这里
    }
);

为了避免丢失被忽略和抛弃的Promise错误,一些开发者表示,Promise链的一个最佳实践就是最后总以一个catch(..)结束,比如:

var p = Promise.resolve( 42 );

p.then(
    function fulfilled(msg){
        // 数字没有string函数,所以会抛出错误,但此处的错误会被吞掉。
        console.log( msg.toLowerCase() );
    },
    function rejected(err){
        // 永远不会到达这里
    }
).catch( handleErrors );

Promise模式

Promise主要用来处理异步流程控制,但同时也有几个变体,使得代码编写更高效,可以使用它们用作构建其他模块的基本块。

当心!若向Promise.all([ .. ])传入空数组,它会立即完成,但Promise. race([ .. ])会挂住,且永远不会决议

Promise.all
  1. 严格说来,传给Promise.all([..])的数组中的值可以是Promise、thenable,甚至是立即值。就本质而言,列表中的每个值都会通过Promise. resolve(..)过滤,以确保要等待的是一个真正的Promise,所以立即值会被规范化为为这个值构建的Promise。如果数组是空的,主Promise就会立即完成。
  2. Promise.all([ .. ])需要一个参数,是一个数组,通常由Promise实例组成。从Promise. all([ .. ])调用返回的promise会收到一个完成消息(代码片段中的msg)。这是一个由所有传入promise的完成消息组成的数组,与指定的顺序一致(与完成顺序无关)。
  3. 从Promise.all([ .. ])返回的主promise在且仅在所有的成员promise都完成后才会完成。如果这些promise中有任何一个被拒绝的话,主Promise.all([ .. ])promise就会立即被拒绝,并丢弃来自其他所有promise的全部结果。
Promise.race
  1. 与Promise.all([..])类似,一旦有任何一个Promise决议为完成,Promise.race([ .. ])就会完成;一旦有任何一个Promise决议为拒绝,它就会拒绝。
  2. 一项竞赛需要至少一个“参赛者”。所以,如果你传入了一个空数组,主race([..]) Promise永远不会决议,而不是立即决议。这很容易搬起石头砸自己的脚!ES6应该指定它完成或拒绝,抑或只是抛出某种同步错误。遗憾的是,因为Promise库在时间上早于ES6 Promise,它们不得已遗留了这个问题,所以,要注意,永远不要递送空数组。