第十一章 Promise与异步编程
Promise可以实现其他语言中类似Future和Deferred一样的功能,是另一种异步编程的选择,它既可以像事件和回调函数一样指定稍后执行的代码,也可以明确指示代码是否成功执行。
第1节 异步编程的背景知识
1. 机制
JavaScript引擎是基于单线程(Single-threaded)事件循环的概念构建,即同一时刻只允许一个代码块在执行。这些代码块被放在一个任务队列(job queue)中,每当一段代码准备执行时,都会被添加到任务队列。每当JavaScript引擎中的一段代码结束执行,事件循环(event loop)会执行队列中下一个任务。事件循环是JavaScript引擎的一段代码,负责监控代码执行并管理任务队列。
2. 事件模型
例如点击按钮或者按下键盘按键会触发的onclick事件,是JavaScript中最基础的异步编程形式。尽管事件模型适用于响应用户交互和完成类似的低频功能,但对更复杂的需求来说却不是很灵活。
3. 回调模式
回调模式和事件模型类似,异步代码都会在未来的某个时间点执行,二者的区别是回调模式中被调用的函数是作为参数传入的。例如:
1 readFile("example.txt", function(err, contents) {
2 if (err) {
3 throw err;
4 }
5 console.log(contents);
6 });
7 console.log("Hi");
相比之下,回调模式比事件模型更灵活,但想要实现更复杂的功能时,回调函数的局限性同样会显现出来。
第2节 Promise的基础知识
Promise相当于异步操作结果的占位符,它不去订阅事件,也不会传递一个回调函数给目标参数,而是让函数返回一个Promise。例如:
1 //readFile承诺将在未来某一个时刻完成
2 let promise = readFile("example.txt");
1. Promise的声明周期
a) Promise先是处于进行中(pending)的状态,此时操作尚未完成;等异步操作执行结束后,Promise变为已处理(settled)状态。分为如下两种状态:
b)Promise的状态改变时,调用then()方法来采取特定的行动。
then()方法接受2个参数:第一个参数是变为fulfilled时要调用的函数,第2个是状态变为rejected时需要调用的函数。
Promise还有一个catch()方法,相当于只给其传入拒绝处理程序的then()方法。
2. 创建未完成的Promise
用Promise构造函数可以创建新的Promise,构造函数只接受一个参数:包含初始化Promise代码的执行器(executor)函数。执行器接受2个参数,分别是执行成功完成时调用的resolve()函数,执行失败时,调用reject()函数。
执行器函数会立即执行,然后才执行后续流程中的代码(即resolve() / reject()会放到任务队列中再执行)。
3. 创建已处理的Promise
使用Promise.resolve() / Promise.reject()来实现根据特定的值来创建已解决的Promise。例如:
1 let promise = Promise.resolve(42);
2 promise.then(function(value) {
3 console.log(value); //42
4 });
5
6 let promise = Promise.reject(42);
7 promise.catch(function(value){
8 console.log(value); // 42
9 });
4. 执行器错误
每个执行器中都隐含一个try-catch块,所以错误会被捕获并传入拒绝处理程序。
第3节 全局的Promise拒绝处理
如果在没有拒绝处理程序的情况下,拒绝一个Promise,那么不会提示信息。Promise的特性决定了很难检测一个Promise是否被处理过。Node.js和浏览器分别做了一些改变来解决开发者这个痛点。
1. Node.js环境的拒绝处理
在Node.js中,处理Promise拒绝时会触发Promise对象上的两个事件:
·unhandledRejection 在一个事件循环中,当Promise被拒绝,并且没有提供拒绝处理程序时,触发该事件。
·rejectionHandled 在一个事件循环后,当Promise被拒绝,若拒绝处理程序被调用,触发该事件。
具体代码参考P.249
2. 浏览器环境的拒绝处理
a) 浏览器也是通过触发两个事件来识别未处理的拒绝的,虽然这些事件是在window对象上触发的,但实际上与Node.js中完全等效。
·unhandledRejection 在一个事件循环中,当Promise被拒绝,并且没有提供拒绝处理程序时,触发该事件。
·rejectionHandled 在一个事件循环后,当Promise被拒绝,若拒绝处理程序被调用,触发该事件。
b) 在Node.js实现中,事件处理程序接受多个参数;而在浏览器中,事件处理程序接受一个有以下属性的事件对象作为参数:
·type 事件名称("unhandledReject"或“rejectionHandled”);
·promise 被拒绝的Promise对象
·reason 来自Promise的拒绝值
c) 浏览器实现的另一处不同是,在两个事件中都可以使用拒绝值(reason)。代码参考:P.251
第4节 串联Promise
记住一个原则:只有当第一个Promise完成或被拒绝后,第二个才会被解决。
1. 捕获错误
在完成处理程序和拒绝处理程序中可能也会发生错误,而Promise链可以用来捕获这些错误。
链式Promise调用可以感知到链中其他Promise的错误。
2. Promise链的返回值
Promise链的另一个重要特性是可以给下游Promise传递数据。在完成处理程序和拒绝处理程序中都可以这么做。
代码参考P.255
3. 在Promise链中返回Promise
先定义的Promise的执行器先执行,后定义的后执行。
第5节 响应多个Promise
之前讲的都是单Promise响应。如果想通过监听多个Promise来决定下一步的操作,可以使用ES6提供的Promise.all()和Promise.race()两个方法来监听多个Promise。
1. Promise.all()方法
该方法只接受一个参数并返回一个Promise,该参数是一个含有多个受监视Promise的可迭代对象,只有当可迭代对象中所有Promise都被解决后,返回的Promise才会被解决,只有当可迭代对象中所有Promise都被完成后返回的Promise才会被完成。
其中,有两种情况:
我们看看这两种情况,分别怎么处理:
1. 当迭代对象中所有的Promise都被解决并返回后,最后的Promise里面存的值按照传入参数数组中的Promise的顺序储存。例如:
1 let p1 = new Promise(function(reolve, reject) {
2 resolve(42);
3 });
4
5 let p2 = new Promise(function(resolve, reject) {
6 resolve(43);
7 });
8
9 let p3 = new Promise(function(resolve, reject) {
10 resolve(44);
11 });
12
13 let p4 = Promise.all([p1, p2, p3]);
14
15 p4.then(function(value) {
16 console.log( Array.isArray(value) ); // true
17 console.log( value[0] ); // 42
18 console.log( value[1] ); // 43
19 console.log( value[2] ); // 44
20 });
2.当迭代对象中有被拒绝的Promise时,只要有一个被拒绝,那么返回的Promise没等所有Promise都完成就立即被拒绝,例如:
1 let p1 = new Promise(function(reolve, reject) {
2 resolve(42);
3 });
4
5 let p2 = new Promise(function(resolve, reject) {
6 reject(43);
7 });
8
9 let p3 = new Promise(function(resolve, reject) {
10 resolve(44);
11 });
12
13 let p4 = Promise.all([p1, p2, p3]);
14
15 p4.then(function(value) {
16 console.log( Array.isArray(value) ); // true
17 console.log( value ); // 43
18 });
2. Promise.race()方法
Promise.race()与Promise.all()稍有不同。在可迭代对象中,只要有一个Promise被解决,返回的Promise就解决,无须等到所有Promise都被完成。
如果先解决的是已完成Promise,则返回已完成Promise;如果先解决的是已拒绝的Promise,则返回已拒绝Promise。看个例子:
1 let p1 = new Promise(function(resolve, reject) {
2 setTimeout(function() {resolve(42);}, 0);
3 });
4
5 let p2 = Promise.reject(43);
6
7 let p3 = new Promise(function(resolve,reject) {
8 resolve(44)
9 });
10
11 let p4 = Promise.race([p1, p2, p3]);
12
13 p4.catch(function(value) {
14 console.log(value); // 43
15 });
第6节 自Promise继承
Promise也是基类,因为也可以派生其它类。
例子参考代码P.262
第7节 基于Promise的异步任务执行
只要每个异步操作都返回Promise,以Promise作为通用接口用于所有异步代码可以简化任务执行器。
例子参考代码P.265
(本节完)