学习Promise异步编程

JavaScript引擎建立在单线程事件循环的概念上。单线程( Single-threaded )意味着同一时刻只能执行一段代码。所以引擎无须留意那些“可能”运行的代码。代码会被放置在作业队列( job queue )中,每当一段代码准备被执行,它就会被添加到作业队列。当 JS 引擎结束当前代码的执行后,事件循环就会执行队列中的下一个作业.事件循环(event loop)是JS引擎的一个内部处理线程,能监视代码的执行并管理作业队列。关于事件循环可以阅读这篇文章 —- 一文梳理JavaScript 事件循环(Event Loop)

1. 为什么要用Promise?

1.1 事件模型

当用户点击一个按钮或按下键盘上的一个键时,一个事件,例如 onclick 就被触发了。该事件可能会对此交互进行响应,从而将一个新的作业添加到作业队列的尾部。这就是 JavaScript 关于异步编程的最基本形式。事件处理程序代码直到事件发生后才会被执行,此时它会拥有合适的上下文。例如:

let button = document.getElementById("my-btn");
button.onclick = function(event) {
	console.log("Clicked");
};

事件可以很好地工作于简单的交互,但将多个分离的异步调用串联在一起却会很麻烦。此外,还需确保所有的事件处理程序都能在事件第一次触发之前被绑定完毕。例如,若 button 在onclick被绑定之前就被点击,那就不会有任何事发生。因此虽然在响应用户交互或类似的低频功能时,事件很有用,但它在面对更复杂的需求时仍然不够灵活

1.2 回调函数

回调函数模式类似于事件模型,因为异步代码也会在后面的一个时间点才执行。不同之处在于需要调用的函
数(即回调函数)是作为参数传入的。

eadFile("example.txt", function(err, contents) {
	if (err) {
		throw err;
	}
	console.log(contents);
});
console.log("Hi!");

使用回调函数模式,readFile() 会立即开始执行,并在开始读取磁盘时暂停。这意味着console.log("Hi!") 会在 readFile() 被调用后立即进行输出,要早于console.log(contents) 的打印操作。当 readFile() 结束操作后,它会将回调函数以及相关参数作为一个新的作业添加到作业队列的尾部。在之前的作业全部结束后,该作业才会执行。回调函数模式要比事件模型灵活得多,因为使用回调函数串联多个调用会相对容易。

这种模式运作得相当好,但容易陷入了回调地狱( callback hell ),这会在嵌套过多回调函数时发生。当想要实现更复杂的功能时,回调函数也会存在问题。如让两个异步操作并行运行,并且在它们都结束后提醒你;同时启动两个异步操作,但只采用首个结束的结果;在这些情况下,需要追踪多个回调函数并做清理操作, Promise 能大幅度改善这种情况。

2. Promise基础

Promise 是异步编程的一种解决方案,相比回调函数和事件,更加强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。

Promise 是为异步操作的结果所准备的占位符。函数可以返回一个 Promise,而不必订阅一个事件或向函数传递一个回调参数。

/ readFile 承诺会在将来某个时间点完成
let promise = readFile("example.txt");

每个 Promise 都会经历一个短暂的生命周期,初始为pending ,这表示异步操作尚未结束。一个状态为pending的 Promise 也被认为是未决的( unsettled )。一旦异步操作结束, Promise就会被认为是已决的(settled),并进入两种可能状态之一:

  • fulfilled(已完成): Promise 的异步操作已成功结束
  • rejected(已拒绝):Promise 的异步操作未成功结束,可能是一个错误,或由其他原因导致

内部的 [[PromiseState]] 属性会被设置为 “pending” 、 “fulfilled” 或 “rejected” ,以反映 Promise 的状态。该属性并未在 Promise 对象上被暴露出来,因此你无法以编程方式判断 Promise 到底处于哪种状态。

2.1 Promise特质及优点

Promise对象有以下两个特点。

(1)对象的状态不受外界影响。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。

(2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。

有了Promise对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise对象提供统一的接口,使得控制异步操作更加容易。

2.2 Promise缺点

  • 无法取消Promise,一旦新建它就会立即执行,无法中途取消。
  • 如果不设置回调函数,Promise内部抛出的错误,不会反应到外部
  • 当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)

3.创建Promise对象

3.1 创建未决的Promise

ES6 规定,Promise对象是一个构造函数,用来生成Promise实例。Promise 新建后就会立即执行。

Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolvereject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。

resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。

function timeout(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, ms, 'done');
  });
}

timeout(100).then((value) => {
  console.log(value); // done
});

Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数。then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为resolved时调用,第二个回调函数是Promise对象的状态变为rejected时调用。这两个函数都是可选的,不一定要提供。它们都接受Promise对象传出的值作为参数。

3.2 创建已决的Promise

使用Promise.resolve()和Promise.reject()方法能够创建已决的Promise对象,前提是传入参数不为pending态的Promise实例,并被Promise.resolve()方法调用。

(1) 参数为空

Promise.resolve()方法调用时不带参数,直接返回一个resolved状态的 Promise 对象。Promise.reject()方法调用时不带参数,直接返回一个rejected状态的 Promise 对象。

(2) 参数为Promise实例

注意:如果传递一个Promise给Promise.resolve(),则不做任何修改、原封不动地返回这个Promise。;传递给Promise.reject(),则会在原 Promise 上包装出一个新的 Promise。示例如下所示:

// 传入Promise状态为resolved
let promise1 = Promise.resolve(43);

let promise2 = Promise.resolve(promise1); // Promise { 43 }
console.log(promise2===promise1); 		// true
promise2.then(function(value){
    console.log(value)      		  // 43
});

let promise3 = Promise.reject(promise1);   // Promise { <rejected> Promise { 43 } }
promise3.catch(function(value){
    console.log(value===promise1) 		 // true
    console.log(value) 					// Promise { 43 }
});


// 传入Promise状态为rejected
let promise4 = Promise.reject(44)

let promise5 = Promise.reject(promise4);
console.log(promise5); // Promise { <rejected> Promise { <rejected> 44 } }

promise5.catch(function(value){
    console.log(value===promise4) // true
    value.catch(function(v){
        console.log(v) 			// 44
    })
});

let promise6 = Promise.resolve(promise4); // Promise {<rejected>: 44}
console.log(promise6===promise4); // true
promise6.catch(function(v){
    console.log(v); // 44
});
// 传入Promise状态为pending
let promise7 = new Promise(function(resolve, reject){
    try{
        resolve();
    }catch (err){
        reject(err);
    }
});
promise7.then(function(){
    console.log('promise7 resolved');
},function(err){
    console.log('promise7 rejected');
});

let promise8 = Promise.resolve(promise7);
console.log(promise8===promise7); 			// true
promise8.then(function(value){
    console.log(value); 				  // undefined
})

let promise9 = Promise.reject(promise7);
console.log(promise9); 				// Promise { <rejected> Promise { undefined } }
promise9.catch(function(value){
    console.log(value===promise7); // true
    console.log(value); 		  // Promise { undefined }
})

(3) 参数为非Promise的Thenable

Promise.resolve() 与 Promise.reject() 都能接受非 Promise 的 thenable 作为参数。

当一个对象拥有一个能接受 resolve 与 reject 参数的 then() 方法,该对象就会被认为是一个非 Promise 的 thenable ,就像这样:

let thenable = {
	then: function(resolve, reject) {
		resolve(42);
	}
};

当传入了非 Promise 的 thenable 时,Promise.resolve()方法会将其转为Promise对象,然后立即执行thenable对象的then()方法。如下所示:

let thenable = {
    then:function(resolve, reject){
        resolve(43);
    }
}

let p1 = Promise.resolve(thenable); // Promise { <pending> }
p1.then(function(value){
    console.log(value); // 43
});

thenable = {
    then:function(resolve, reject){
        reject(44);
    }
}
let p2 = Promise.resolve(thenable) // // Promise { <pending> }
p2.catch(function(value){
    console.log(value); // 44
});

// p1,p2 等同于 new Promise(function(resolve, reject){
//     try{
//         resolve(43);
//     } catch (err) {
//         reject(44)
//     }
// });

当传入了非 Promise 的 thenable 时,Promise.reject()方法则会在thenable对象上包装出一个Promise,状态为rejected,调用该Promise的catch方法则其value参数为thenable对象。

let thenable = {
    then:function(resolve, reject){
        resolve(43);
    }
}

let p1 = Promise.reject(thenable); // Promise { <rejected> { then: [Function: then] } }
console.log(p1)
p1.catch(function(value){
    console.log(value=== thenable); // true
});

thenable = {
    then:function(resolve, reject){
        reject(44);
    }
}
let p2 = Promise.reject(thenable)    // Promise { <rejected> { then: [Function: then] } }
p2.catch(function(value){
    console.log(value===thenable); // true
});

(4) 参数为不具有then方法

如果参数是一个原始值,或者是一个不具有then()方法的对象,则Promise.resolve()方法返回一个新的 Promise 对象,状态为resolvedPromise.reject()方法返回一个状态为rejected的Promise对象。

4. 单异步响应

Promise实例具有3个原型方法,用以平时处理单个异步操作,如下所示:

  • Promise.prototype.then
  • Promise.prototype.catch
  • Promise.prototype.finally

4.1 Promise.prototype.then

then方法是定义在原型对象Promise.prototype上的。作用是为 Promise 实例添加状态改变时的回调函数。前面说过,then方法的第一个参数是resolved状态的回调函数,第二个参数是rejected状态的回调函数,它们都是可选的。

then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。采用链式的then,可以指定一组按照次序调用的回调函数。这时,前一个回调函数,有可能返回的还是一个Promise对象(即有异步操作),这时后一个回调函数,就会等待该Promise对象的状态发生变化,才会被调用。

4.2 Promise.prototype.catch

Promise.prototype.catch()方法等同于.then(null, rejection).then(undefined, rejection),用于指定发生错误时的回调函数。所以catch方法返回的也是一个新的Promise实例,也可以采用链式写法。

如果异步操作抛出错误,Promise状态变为rejected,就会调用catch()方法指定的回调函数,处理这个错误。另外,then()方法指定的回调函数,如果运行中抛出错误,也会被catch()方法捕获。

Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获。跟传统的try/catch代码块不同的是,如果没有使用catch()方法指定错误处理的回调函数Promise 对象抛出的错误不会传递到外层代码,即不会有任何反应。

一般来说,不要在then()方法里面定义 Reject 状态的回调函数(即then的第二个参数),总是使用catch方法。

// bad
promise
  .then(function(data) {
    // success
  }, function(err) {
    // error
  });

// good
promise
  .then(function(data) { //cb
    // success
  })
  .catch(function(err) {
    // error
  });

4.3 Promise.prototype.finally

finally()方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。finally方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled还是rejected。这表明,finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。

promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});

上面代码中,不管promise最后的状态,在执行完thencatch指定的回调函数以后,都会执行finally方法指定的回调函数。

finally本质上是then方法的特例。

promise
.finally(() => {
  // 语句
});

// 等同于
promise
.then(
  result => {
    // 语句
    return result;
  },
  error => {
    // 语句
    throw error;
  }
);

上面代码中,如果不使用finally方法,同样的语句需要为成功和失败两种情况各写一次。有了finally方法,则只需要写一次。

它的实现也很简单。

Promise.prototype.finally = function (callback) {
  let P = this.constructor;
  return this.then(
    value  => P.resolve(callback()).then(() => value),
    reason => P.resolve(callback()).then(() => { throw reason })
  );
};

上面代码中,不管前面的 Promise 是fulfilled还是rejected,都会执行回调函数callback

5. 并行异步响应

JavaScript中Promise有如下方法可并行处理多个异步操作:

  • Promise.all()
  • Promise.race()
  • Promise.allSettled()
  • Promise.any()

5.1 Promise.all()

Promise.all()方法接收单个可迭代对象(如数组)作为参数,可迭代对象的元素都为Promise实例,若不是则调用Promise.resolve()方法将其转化为Promise实例,再进一步处理。

const p = Promise.all([p1,p2,p3])

p的状态由p1p2p3决定,分成两种情况。

(1)只有p1p2p3的状态都变成fulfilledp的状态才会变成fulfilled,此时p1p2p3的返回值组成一个数组,传递给p的回调函数。

(2)只要p1p2p3之中有一个被rejectedp的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

5.2 Promise.race()

Promise.race()也接受一个包含需监视的 Promise 的可迭代对象,并返回一个新的 Promise。和Promise.all()方法不同的是,一旦来源Promise中有一个被完成,所返回的Promise就会立刻完成,那个率先完成得Promsie的返回值会传递给返回的Promise对象。

let p1 = Promise.resolve(42);
let p2 = new Promise(function(resolve, reject) {
	resolve(43);
});
let p3 = new Promise(function(resolve, reject) {
	resolve(44);
});
let p4 = Promise.race([p1, p2, p3]);
p4.then(function(value) {
	console.log(value); // 42
});

5.3 Promise.allSettled()

Promise.allSettled()方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。只有等到所有这些参数实例都返回结果,不管是fulfilled还是rejected,包装实例才会结束。该方法由 ES2020 引入。

该方法返回的新的 Promise 实例,一旦结束,状态总是fulfilled,不会变成rejected。状态变成fulfilled后,Promise 的监听函数接收到的参数是一个数组,每个成员对应一个传入Promise.allSettled()的 Promise 实例。

let p1 = Promise.resolve(42);
let p2 = new Promise(function(resolve, reject) {
	resolve(43);
});
let p3 = new Promise(function(resolve, reject) {
	reject(44);
});
let p4 = Promise.allSettled([p1, p2, p3]);
p4.then(function(value) {
  console.log(value);
  console.log(p4);
});

0: {status: "fulfilled", value: 42}
1: {status: "fulfilled", value: 43}
2: {status: "rejected", reason: 44}

5.4 Promise.any()

ES2021 引入了Promise.any()方法。该方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例返回。只要参数实例有一个变成fulfilled状态,

const p = Promise.any([p1,p2,p3]);

p的状态由p1p2p3决定,分成两种情况。

(1)只要p1p2p3的状态任意一个变成fulfilledp的状态就变成fulfilled,并且首个fulfilled的Promise的返回值传递给p的回调函数。

(2)只有p1p2p3全部被rejectedp的状态就变成rejected,并且抛出一个AggregateError 错误。它相当于一个数组,每个成员对应一个被rejected的操作所抛出的错误。

let p1 = Promise.resolve(42);
let p2 = new Promise(function(resolve, reject) {
	resolve(43);
});
let p3 = new Promise(function(resolve, reject) {
	reject(44);
});
let p = Promise.any([p1, p2, p3]);
p.then(function(value) {
  console.log(value);
  console.log(p);
});

results

let p1 = Promise.reject(42);
let p2 = new Promise(function(resolve, reject) {
	reject(43);
});
let p3 = new Promise(function(resolve, reject) {
	reject(44);
});
let p = Promise.any([p1, p2, p3]);
p.then(function(value) {
  console.log(value);
});

results:

6.小结

Promise被设计用于改善JS中的异步编程,与事件和回调函数对比,在异步操作中给我们提供了更多的控制权与组合型。Promise具有三种状态:挂起、已完成、已拒绝。一个Promise起始于挂起态,并在成功时转为完成态,或在失败时转为拒绝态。在这两种情况下,处理函数都能被添加以表明Promise何时被解决。then()方法允许你绑定完成处理函数与拒绝处理函数,而 catch()方法则只允许你绑定拒绝处理函数。并且Promise能用多种方式串联在一起,并在它们之间传递信息。每个对 then() 的调用都创建并返回了一个新的Promise,在前一个Promise被决议时,新Promise也会被决议。Promise链可被用于触发对一系列异步事件的响应。除此之外,我们能够使用Promsie.all()/Promise.race()/Promise.allSettled()/Promise.any()同时监听多个Promise,并行性相应的响应。