第十一章、Promise与异步编程
1、异步编程的背景知识
JavaScript引擎是基于单线程事件循环的概念构建的,同一时刻只允许一个代码块执行。
即将运行的代码块都是被放在任务队列(job queue)中,当JavaScript引擎一段代码执行结束时,事件循环(event loop)会将任务队列的第一个代码块交给JavaScript引擎进行执行,知道任务队列中的任务执行完毕。
1、事件模型
事件模型适用于处理简单的交互,然而将多个异步调用连在一起会是程序更加复杂,事件模型就不灵活了。
2、回调函数
Node.js通过回调函数处理异步编程,但与事件模型类似,异步代码都会在未来某个时间点执行,两个的区别就是回调函数中被调用的函数是作为参数被传入的。
但回调函数的缺点就是层层嵌套,最终会让你陷入回调地狱。而且没法执行更复杂的情况,例如:
并行执行两个异步操作,当两个操作都结束时执行某个操作;并行执行两个异步操作,只取优先完成的操作结果等。需要同时监测多个回调函数,惨不忍睹。
2、Promise的基础知识
Promise相当于异步操作结果的占位符。
1、Promise的生命周期
pending(进行中)---> fulfilled(成功完成)/ rejected(失败完成)
内部属性[[PromiseState]]被用来表示Promise的三种状态:"pending"/"fulfilled"/"rejected",该属性不暴露在Promise对象上,只能通过then()方法采取特定的行动。
所有Promise都有then()方法,有两个可选参数:第一个是操作成功时要执行的成功完成函数;第二个是操作失败时要执行的失败完成函数。
备注:如果一个对象实现了上述的then()方法,那这个对象被称为thenable对象。
所有的Promise都是thenable对象,但并非所有的thenable对象都是Promise。
Promise还有一个catch()方法,参数等同于then()方法的第二个参数。
如果一个Promise处于已处理状态,在这之后添加到任务队列中的处理程序仍将执行。所以无论何时都可以添加新的完成处理程序和拒绝处理程序,同时还能保证这些处理程序都能被执行。
let promise = readFile("text.txt");
// 最初的完成处理程序
promise.then(function(contents){
console.log(contents);
// 又添加一个新的依旧会被执行
promise.then(function(contents){
console.log(contents);
});
});
备注:每次调用then()方法和catch()方法都会创建一个新任务,当Promise被resolved时执行。这些任务最终会被加入到一个Promise队列。
2、创建未完成的Promise
用Promise构造函数创建Promise,构造函数接收一个参数:包含初始化Promise代码的执行器函数。
执行器函数接收两个参数:resolve()函数和reject()函数。
let fs = require("fs");
function readFile(fileName){
return new Promise(function(resolve,reject){
fs.readFile(fileName,{encoding:"utf8"},function(error,contents){
if(error){
reject(error);
return ;
}
resolve(contents);
});
});
}
let promise = readFile("text.txt");
promise.then(function(contents){
console.log(contents);
},function(error){
console.log(error);
});
执行器在实例被创建时会立即执行,但完成处理程序和拒绝处理程序总是在执行器完成后被添加到任务队列的末尾异步执行。
实例:
let promise = new Promise(function(resolve,reject){
console.log("Promise");
resolve();
});
promise.then(function(){
console.log("Resolved");
});
console.log("HI");
最终的输出结果是:
Promise
HI
Resolved
3、创建已处理的Promise
①、使用Promise.resolve()
Promise.resolve()方法只接受一个参数并返回一个完成态的Promise。
let promise = Promise.resolve(42);
promise.then(function(value){
console.log(value); // 42
});
②、使用Promise.reject()
Promise.reject()方法只接受一个参数并返回一个拒绝态的Promise。
let promise = Promise.reject(42);
promise.catch(function(value){
console.log(value); // 42
});
备注:如果向Promise.resolve()或者Promise.reject()方法传入一个Promise,这个Promise会被直接返回。
③、非Promise的Thenable对象
Promise.resolve()方法和Promise.reject()方法都接受非Promise的Thenable对象作为参数,且返回一个新的Promise。
拥有then()方法并且接受resolve和reject这两个参数的普通对象就是非Promise的Thenable对象。
let thenable1 = {
then:function(resolve,reject){
resolve(42);
}
};
let thenable2 = {
then:function(resolve,reject){
reject(32);
}
};
let p1 = Promise.resolve(thenable1);
let p2 = Promise.reject(thenable2);
p1.then(function(value){
console.log(value); // 42
});
p2.then(function(value){
console.log(value); // 32
});
4、执行器错误
如果执行器内部抛出了一个错误,则Promise的拒绝处理程序就会被调用。
let promise = new Promise(function(resolve,reject){
throw new Error("Explosion!");
});
等价于
let promise = new Promise(function(resolve,reject){
try{
throw new Error("Explosion!");
}catch(ex){
reject(ex);
}
});
promise.catch(function(error){
console.log(error.message); // "Explosion!"
});
3、全局的Promise拒绝处理
有关Promise有一个最具争议的问题:如果在没有拒绝处理程序reject的情况下,如何拒绝一个Promise呢?
Promise的特性决定了很难监测一个Promise是否被处理过。
1、Node.js环境的拒绝处理
在Node.js中,处理Promise拒绝时会触发process对象上的两个事件:
①、unhandledRejection:在一个事件循环中,当Promise被拒绝,并且没有提供拒绝处理程序时,触发该事件。
②、rejectionHandled:在一个事件循环后,当Promise被拒绝,且拒绝处理程序被调用时,触发该事件。
①、拒绝原因和被拒绝的Promise作为参数被传入unhandledRejection事件处理程序中:
let rejected;
process.on("unhandledRejection",function(reason,promise){
console.log(reason.message); // "Explosion!"
console.log(rejected === promise); // true
});
rejected = Promise.reject(new Error("Explosion!"));
②、rejectionHandled事件处理程序只有一个参数,即被拒绝的Promise:
let rejected;
process.on("rejectionHandled",function(promise){
console.log(rejected === promise); // true
});
rejected = Promise.reject(new Error("Explosion!"));
// 等待当前事件循环中rejectionHandled事件注册成功,在下一事件循环中执行拒绝处理程序
setTimeout(function(){
rejected.catch(function(value){
console.log(value.message); // "Explosion!"
});
},1000);
一个简单的未处理拒绝跟踪器:
let possiblyUnhandledRejections = new Map();
process.on("unhandledRejection",function(reason,promise){
possiblyUnhandledRejections.set(promise,reason);
});
process.on("rejectionHandled",function(promise){
possiblyUnhandledRejections.delete(promise);
});
setInterval(function(){
possiblyUnhandledRejections.forEach(function(reason,promise){
console.log(reason.message?reason.message:reason);
handleRejection(promise,reason);
});
possiblyUnhandledRejections.clear();
},60000);
2、浏览器环境的拒绝处理
浏览器处理拒绝的方式与Node.js处理拒绝的方式相似,唯一的区别是浏览器中这些事件是在window对象上触发的。
事件处理程序接受一个具有以下属性的事件对象作为参数:
type:事件名称(unhandledrejection或rejectionhandled)
promise:被拒绝的Promise对象
reason:来自Promise的拒绝值
let rejected;
window.onunhandledrejection = function(event){
console.log(event.type); // "unhandledrejection"
console.log(event.reason.message); // "Explosion!"
console.log(rejected === event.promise); // true
};
window.onrejectionhandled = function(event){
console.log(event.type); // "rejectionhandled"
console.log(event.reason.message); // "Explosion!"
console.log(rejected === event.promise); // true
};
rejected = Promise.reject(new Error("Explosion!"));
一个简单的未处理拒绝跟踪器:
let possiblyUnhandledRejections = new Map();
window.onunhandledrejection = function(event){
possiblyUnhandledRejections.set(event.promise,event.reason);
};
window.onrejectionhandled = function(event){
possiblyUnhandledRejections.delete(event.promise);
};
setInterval(function(){
possiblyUnhandledRejections.forEach(function(reason,promise){
console.log(reason.message?reason.message:reason);
handleRejection(promise,reason);
});
possiblyUnhandledRejections.clear();
},60000);
4、串联Promise
每次调用then()或catch()时实际上创建并返回了另一个Promise,只有当第一个Promise完成或被拒绝后,第二个才会被解决。
let P1 = new Promise(function(resolve,reject){
resolve(42);
});
p1.then(function(value){
console.log(value);
}).then(function(){
console.log("finished!");
});
输出结果:
42
"finished!"
1、捕获错误
Promise链可以用来捕获完成处理程序或拒绝处理程序中可能出现的错误。
链式Promise调用可以感知链中其他Promise的错误。
let p1 = new Promise(function(resolve,reject){
resolve(42);
});
p1.then(function(value){
console.log(value);
throw new error("Explosion!");
}).catch(function(error){
console.log(error.message);
});
输出结果:
42
"Explosion!"
备注:务必在Promise链的末尾留一个拒绝处理程序以确保能够正确处理所有可能发生的错误。
2、Promise链的返回值
Promise链的一个重要特性是可以给下游Promise传递数据。如果在完成处理程序中指定一个返回值,则可以沿着这条链继续传递数据。
let p1 = new Promise(function(resolve,reject){
reject(42);
});
p1.catch(function(value){
console.log(value);
return value + 1;
}).then(function(value){
console.log(value);
});
输出结果:
42
43
拒绝处理程序返回的值仍可用在下一个Promise的完成处理程序中,在必要时,即使其中一个Promise失败也能恢复整条链的执行。
3、在Promise链中返回Promise
let p1 = new Promise(function(resolve,reject){
resolve(42);
});
let p2 = new Promise(function(resolve,reject){
reject(43);
});
p1.then(function(value){
console.log(value); // 42
return p2;
}).then(function(value){
console.log(value); // 从未被调用
}).catch(function(value){
console.log(value); // 43
});
在完成处理程序或拒绝处理程序中返回Thenable对象不会改变Promise执行器的执行时机,先定义的Promise的执行器先执行,后定义的后执行,以此类推。
返回Thenable对象仅仅允许为这些Promise结果定义额外的响应。在完成处理程序中创建新的Promise可以推迟完成处理程序的执行。
let p1 = new Promise(function(resolve,reject){
resolve(42);
});
p1.then(function(value){
console.log(value); // 42
return new Promise(function(resolve,reject){
reject(43);
});
}).then(function(value){
console.log(value); // 43
});
5、响应多个Promise
可以通过ES6提供的Promise.all()方法和Promise.race()方法来监听多个Promise.
1、Promise.all()方法
Promise.all()方法只接受一个参数并且返回一个Promise,该参数是一个含有多个受监视的Promise的可迭代对象,只有当可迭代对象中的所有Promise都被完成后,返回的Promise才会被完成。
let p1 = new Promise(function(resolve,reject){
resolve(42);
});
let p2 = new Promise(function(resolve,reject){
resolve(43);
});
let p3 = new Promise(function(resolve,reject){
resolve(44);
});
let p4 = Promise.all([p1,p2,p3]);
p4.then(function(value){
console.log(Array.isArray(value)); // true
console.log(value[0]); // 42
console.log(value[1]); // 43
console.log(value[2]); // 44
});
所有传入Promise.all()方法的Promise只要有一个被拒绝,那么返回的Promise没等所有Promise都完成就立即被拒绝。
let p1 = new Promise(function(resolve,reject){
resolve(42);
});
let p2 = new Promise(function(resolve,reject){
reject(43);
});
let p3 = new Promise(function(resolve,reject){
resolve(44);
});
let p4 = Promise.all([p1,p2,p3]);
p4.catch(function(value){
console.log(Array.isArray(value)); // false
console.log(value); // 43
});
拒绝处理程序总是接收一个值而非数组,该值来自被拒绝Promise的拒绝值。
2、Promise.race()方法
Promise.race()方法只接受一个参数并且返回一个Promise,该参数是一个含有多个受监视的Promise的可迭代对象,但只要有一个Promise被解决,返回的Promise就被解决,不用等到所有Promise都完成。
传给Promise.race()方法的Promise会进行竞选.
如果先解决的是已完成Promise,则返回已完成Promise;如果先解决的是已拒绝Promise,则返回已拒绝Promise.
let p1 = Promise.resolve(42);
let p2 = new Promise(function(resolve,reject){
reject(43);
});
let p3 = new Promise(function(resolve,reject){
resolve(44);
});
let p4 = Promise.all([p1,p2,p3]);
p4.then(function(value){
console.log(value); // 42
});
6、自Promise继承
Promise与其他内建类型一样,也可以用来继承。例如:
class MyPromise extends Promise{
// 构造函数使用默认的
success(resolve){
return this.then(resolve);
}
failure(reject){
return this.catch(reject);
}
fulfill(resolve,reject){
return this.then(resolve,reject);
}
}
let p1 = new MyPromise(function(resolve,reject){
resolve(42);
reject(43);
});
p1.success(function(value){
console.log(value); // 42
}).failure(function(value){
console.log(value); // 43
});
由于静态方法会被继承,因此派生的Promise也拥有MyPromise.resolve()、MyPromise.reject()、MyPromise.all()、MyPromise.race()方法。
MyPromise.resolve()和MyPromise.reject()方法与内建的稍有不同,他们返回的是MyPromise的实例。
如果将内建Promise传入这两个方法,那么这个Promise会被解决,同时返回一个新的MyPromise。
7、基于Promise的异步任务执行
let fs = require("fs");
function run(taskDef){
// 创建迭代器
let task = taskDef();
// 开始执行任务
let result = task.next();
// 递归遍历
(function step(){
// 如果有多个任务
if(!result.done){
let promise = Promise.resolve(result.value);
promise.then(function(value){
result = task.next(value);
step();
}).catch(function(error){
result = task.throw(error);
step();
});
}
}())
}
// 定义一个可用于任务执行器的函数
function readFile(fileName){
return new Promise(function(resolve,reject){
fs.readFile(fileName,function(error,contents){
if(error){
reject(error);
}else{
resolve(contents);
}
});
});
}
// 执行一个任务
run(function*(){
let contents = yield readFile("config.json");
doSomeThing(contents);
});
备注:未来的异步任务执行
其基本思想是用async标记的函数代替生成器,用await代替yield来调用函数。
run(async function(){
let contents = await readFile("config.json");
doSomeThing(contents);
});
即同步方式编写异步代码的思想。