ES6异步操作-Promise和异步编程
前言
JS引擎是基于单线程(Single-threaded)事件循环的概念构建的,同一时刻只允许一个代码块在执行,所以需要跟踪即将运行的代码,那些代码被放在一个任务队列(job queue)中,每一段代码准备执行时,都会被添加到任务队列。每当JS引擎中的一段代码结束执行,事件循环(event toop)会执行队列中的下一个任务,它是JS引擎中的一段程序,负责监控代码执行并管理任务队列,队列中的任务会从第一个一直执行到最后一个
事件模型
单击按钮时触发onclick事件,它会向任务队列中添加一个新任务来响应用户的操作,这个任务要到任务队列中的排在前面的任务都完成后才会执行,这是JS中最基础的异步编程形式。
btn.onclick = function() {
// doSomething
}
事件模型用于处理简单的交互,由于必须要跟踪每个事件的目标对象,所以在处理复杂异步任务时很不灵活
回调模式
Nodejs通过使用回调函数改进异步编程模型,异步代码会在未来的某个时刻执行,与事件模型不同的是回调模式中被调用的函数是作为参数传入的
let fs = require('fs')
fs.readFile('config.json', function(err, contents){
if(err) throw err;
doSomething(contents);
})
console.log('hello')
Nodejs采用错误优先(error-first)的回调风格,当调用readFile()函数后,并不会阻塞console.log('hello')
的执行。readFile()执行结束后,会向任务队列的末尾添加一个新任务,任务包含回调函数及相应的参数,当队列前面所有的任务完成后才执行该任务
回调模式比事件模型要灵活,但它也有问题,如果回调模式中嵌套了很多其他异步操作,便会陷入回调地狱,Promise的出现就是为了改进这样的情况
基础
Promise相当于异步操作结果的占位符,它会让函数返回一个Promise
// readFile承诺会在未来某个时间完成任务
let promise = readFile('config.json');
示例中的readFile()不会立即读取文件,而是先返回一个表示异步读取操作的Promise对象,未来对这个对象的操作完全取决于Promise的声明周期
Promise声明周期
每个Promise都会经历一个短暂的声明周期:先是进行中(pending)状态,然后是未处理(unsettled)状态,这两步操作异步任务都尚未完成;直到Promise变为已处理(settled)的状态,异步操作执行才结束
示例中readFile()返回promise时处于pending状态,操作结束后Promise可能进入下面两个状态中的一个
- Fulfilled 表示Promise异步操作成功完成
- Rejected 表示Promise异步操作未能成功,可能是程序错误或其他原因导致的
Promise内部有一个[[PromiseState]]属性用来表示Promise的3中状态:pending、fulfilled、rejected。这个属性不能通过编程的方式检测Promise的状态,但可以通过then()方法采取特定的行动
所有Promise都有then()方法,它接受两个参数:第一个是当Promise的状态变为fulfilled时要调用的函数,所有与完成状态相关的附加数据都会传递给这个完成函数(fulfillment function);第二个是当Promise的状态变为rejected时要调用的函数,所有与失败状态相关的附加数据都会传递给这个拒绝函数(rejection function)。这两个参数都是可选的,所以可以按照任意组合的方式来监听Promise
let promise = readFile('config.json');
// 示例1
promise.then(function(contents){
// 完成
console.log(contents)
}, function(err){
// 拒绝
console.error(err.message)
})
// 示例2
promise.then(function(contents){
// 完成
console.log(contents)
})
// 示例3
promise.then(null,function(err){
// 拒绝
console.error(err.message)
})
Promise还有一个catch()方法,相当于只给其传入拒绝处理程序的then()方法
promise.catch(function(err) {
// 拒绝
console.error(err.message);
});
// 等同于:
promise.then(null, function(err) {
// 拒绝
console.error(err.message);
});
注意: 每次调用then()方法或catch()方法都会创建一个新任务,这些任务最终会被加入到一个为Promise量身定制的独立队列中
创建未完成的Promise
可以用Promise构造函数创建新的Promise,构造函数接收一个函数作为参数,这个函数叫做执行器函数。执行器函数接收两个参数:resolve参数,执行器成功完成时调用的函数;reject参数,执行器失败时调用的函数
let fs = require('fs');
function readFile(filename) {
return new Promise(function(resolve, reject) {
// 异步任务
fs.readFile(filename, function(err, contents){
if(err) return reject(err); // 失败
resolve(contents); // 成功
})
})
}
let promise = readFile('config.json');
promise.then(function(contents){
console.log(contents); // 完成
}, function(err){
console.log(err.message); // 拒绝
})
readFile()方法被调用时执行器会立刻执行,在执行器中,无论是调用resolve()还是reject(),都会向任务队列中添加一个任务来解决这个Promise,resolve()和reject()的执行都是异步的。
let promise = new Promise(function(resolve, reject){
console.log('hello'); // 非异步
resolve(); // 异步
})
promise.then(function(){
console.log('ok')
})
console.log('world')
// hello
// world
// ok
ok在world之后被打印,这是因为resolve处理函数和reject处理函数总是在执行器完成后才被添加到任务队列的末尾
创建已处理的Promise
如果想用Promise来表示一个已知值,可以用以下两种方法根据特定的值来创建己解决Promise
【Promise.resolve()】
Promise.resolve()方法只接收一个参数,并返回一个完成态的Promise
let promise = Promise.resolve(3);
promise.then(function(value) {
console.log(value)
})
// 3
【Promise.reject()】
Promise.reject()方法只接收一个参数,并返回一个拒绝态的Promise
let promise = Promise.reject(3);
promise.catch(function(value) {
console.log(value)
})
// 3
注意: 如果向Promise.resolve()方法或Promise.reject()方法传入一个Promise,那么这个Promise会被直接返回
非Promise的Thenable对象
拥有then()方法并且接受resolve和reject这两个参数的普通对象就是非Promise的Thenable对象。所有的Promise都是thenable对象,但并非所有thenable对象都是Promise
let thenable = {
then: function(resolve, reject) {
resolve(3);
}
}
let p = Promise.resolve(thenable);
p.then(function(value){
console.log(value)
})
// 3
Promise.resolve()方法和Promise.reject()方法都可以接受非Promise的Thenable对象作为参,如果传入一个非Promise的Thenable对象,则这些方法会创建一个新的Promise,并在then()函数中被调用
示例中的Thenable对象通过Promise.resolve()方法转换成了一个完成态的Promise
也可以使用Promise.resolve()创建基于Thenble对象的拒绝态Promise
let thenable = {
then: function(resolve, reject) {
reject(3);
}
}
let p = Promise.resolve(thenable);
p.catch(function(value){
console.log(value)
})
// 3
执行器错误
如果执行器的内部抛出了错误,那么Promise的拒绝处理程序就会被调用
let promise = new Promise(function(resolve, reject){
throw new Error('Explosion');
})
promise.catch(function(err){
console.log(err.message) // Explosion
})
每个Promise执行器都隐含了一个try-catch块,所以错误会被捕获并传入拒绝处理程序。上面的示例等价于
let promise = new Promise(function(resolve, reject){
try{
throw new Error('Explosion');
}catch(err){
reject(err)
}
})
promise.catch(function(err){
console.log(err.message) // Explosion
})
串联
每次调用then()方法或catch()方法时实际上创建并返回了一个Promise,只有第一个Promise完成或被拒绝后,第二个Promise才会被解决
let promise = new Promise(function(resolve, reject){
resolve(3);
})
promise.then(function(value){
console.log(value)
}).then(function(){
console.log(4)
})
// 3
// 4
示例中调用promise.then()后返回第二个Promise,只有第一个Promise被解决后才会调用第二个then()方法的完成处理程序
捕获错误
完成处理程序或拒绝处理程序都可能发生错误,可以通过Promise链捕获这些错误
// 完成处理程序抛出错误
let promise = new Promise(function(resolve, reject){
resolve(3)
})
promise.then(function(value){
throw new Error('Boom')
}).catch(function(err){
console.log(err.message) // Boom
})
示例中,完成处理程序中的Promise抛出了错误,错误直接在紧邻的catch()方法中被捕获
// 拒绝处理程序抛出错误
let promise = new Promise(function(resolve, reject){
throw new Error('Explosion')
})
promise.catch(function(error){
console.log(error.message) // Explosion
throw new Error('Boom')
}).catch(function(err){
console.log(err.message) // Boom
})
示例中,拒绝处理程序中的Promise抛出了错误,错误触发了promise的拒绝处理程序,处理程序又抛出了另外的错误,并被第二个拒绝处理函数捕获,链式调用的Promise可以感知到其他Promise的错误
注意: 务必在Promise链的末尾留有一个拒绝处理程序,以确保能够正确处理所有可能发生的错误
Promise链的返回值
Promise链的一个重要特性是可以给下游Promise传递数据,如果在完成处理程序或拒绝处理程序中指定一个返回值,则可以沿着这条链继续传递数据
// 示例1
let promise = new Promise(function(resolve, reject){
resolve(3)
})
promise.then(function(value){
return value + 1;
}).then(function(value){
console.log(value); // 4
})
// 示例2
let promise = new Promise(function(resolve, reject){
reject(3)
})
promise.catch(function(value){
return value + 1;
}).then(function(value){
console.log(value); // 4
})
在Promise链中返回Promise
let p1 = new Promise(function(resolve, reject){
resolve(3)
})
let p2 = new Promise(function(resolve, reject){
resolve(4)
})
p1.then(function(value){
console.log(value)
return p2
}).then(function(value){
console.log(value)
})
// 3
// 4
示例中,在p1的Promise链中返回了一个p2,p2是一个完成态(已解决状态)的Promsie,所以可以在第二个完成处理程序中调用。如果p2是拒绝态的Promise,则可以在拒绝处理程序中被调用。
let p1 = new Promise(function(resolve, reject){
resolve(3)
})
let p2 = new Promise(function(resolve, reject){
reject(4) // 拒绝
})
p1.then(function(value){
console.log(value)
return p2
}).catch(function(value){
console.log(value)
})
// 3
// 4
示例中的p2是一个拒绝态的Promise,它在Promise链中被拒绝处理程序调用。
关于这个模式最需要注意的是,第二个完成处理程序或拒绝处理程序,被添加到了第三个Promise链中而不是第二个
let p1 = new Promise(function(resolve, reject){
resolve(3)
})
let p2 = new Promise(function(resolve, reject){
resolve(4)
})
let p3 = p1.then(function(value){
console.log(value) // 3
return p2
})
p3.then(function(value){
console.log(value) // 4
})
示例中p2的完成处理程序,被添加到了p3的Promise链中,所以可以在p3的完成处理程序中被被调用
可以在Promise链的完成处理程序或拒绝处理程序中,返回一个Thenable对象,先定义的Promise的执行器先执行,后定义的后执行。
let p1 = new Promise(function(resolve, reject){
resolve(3)
})
p1.then(function(value){
console.log(value) // 3
// 返回一个Thenable对象
let p2 = new Promise(function(resolve, reject){
resolve(4)
})
return p2
}).then(function(value){
console.log(value) // 4
})
如果想在一个Promise被解决后触发另个promise,那么这个模式会很有帮助
响应多个
如果想通过监听多个Promise来决定下一步的操作,可以使用ES6提供的Promise.all()和Promise.race()这两个方法
Promise.all()
Promise.all()方法只接受一个参数并返回一个Promsie,该参数是一个含有多个受监视Primise的可迭代对象,只有当可迭代对象中的所有Promise都被解决后返回的Promsie才会被解决,只有当可迭代对象中所有Promsie都被完成后返回的Promise才会被完成
let p1 = new Promise(function(resolve, reject) {
resolve(3)
})
let p2 = new Promise(function(resolve, reject) {
resolve(4)
})
let p3 = new Promise(function(resolve, reject) {
resolve(5)
})
let p4 = Promise.all([p1, p2, p3])
p4.then(function(value) {
console.log(Array.isArray(value)) // true
console.log(value[0]) // 3
console.log(value[1]) // 4
console.log(value[2]) // 5
})
示例中,只有p1、p2、p3都处于完成状态后p4才被完成,传入p4的完成处理程序会按照顺序被解决
所有传入Promise.all()方法的Promise只要有一个被拒绝,那么返回的Promise没等所有Promise都完成就立即被拒绝
let p1 = new Promise(function(resolve, reject) {
resolve(3)
})
let p2 = new Promise(function(resolve, reject) {
reject(4) // 拒绝
})
let p3 = new Promise(function(resolve, reject) {
resolve(5)
})
let p4 = Promise.all([p1, p2, p3])
p4.catch(function(value) {
console.log(Array.isArray(value)) // false
console.log(value) // 4
})
示例中,p2传入了拒绝处理程序,p4的拒绝处理程序会立即被调用而不会等待p1或p3执行完成(p1和p3的执行过程会结束,只是p4并未等待)。拒绝处理程序总是接受一个值而非数组,该值来自被拒绝Promise的拒绝值
Promise.race()
Promise.race()也接收多个受监视Promise的可迭代对象作为唯一参数,并返回一个Promise。但是只要有一个Promise被解决(无论是完成处理程序还是拒绝处理程序)返回的Promise就被解决,而无需等待其他Promise都被解决。这有点像竞赛,如果先解决的是已完成Promise,则返回己完成Promise;如果先解决的是已拒绝Promise,则返回已拒绝Promise
let p1 = new Promise(function(resolve, reject) {
setTimeout(function(){
resolve(3)
}, 200)
})
let p2 = new Promise(function(resolve, reject) {
setTimeout(function(){
resolve(4)
}, 100)
})
let p3 = new Promise(function(resolve, reject) {
setTimeout(function(){
resolve(5)
}, 300)
})
let p4 = Promise.race([p1,p2,p3])
p4.then(function(value){
console.log(value) // 4
})
示例中模拟了异步操作,p2用时最短,所以p4的完成处理程序最先返回了4,其他Promise的结果则被忽略。
let p1 = new Promise(function(resolve, reject) {
setTimeout(function(){
resolve(3)
}, 200)
})
let p2 = new Promise(function(resolve, reject) {
setTimeout(function(){
reject(4) // 拒绝
}, 100)
})
let p3 = new Promise(function(resolve, reject) {
setTimeout(function(){
resolve(5)
}, 300)
})
let p4 = Promise.race([p1,p2,p3])
p4.catch(function(value){
console.log(value) // 4
})
对于拒绝处理程序也是一样,由于p2用时最短,所以p4的拒绝处理程序最先返回了4
继承
Promise与其他内建类型一样,也可以作为基类派生其他类,所以可以定义自己的Promsie变量扩展内建Promise的功能
下面创建一个即支持then()方法和catch()方法,又支持success()和failure()方法的Promise
class MyPromise extends Promise {
success(resolve, reject) {
return this.then(resolve, reject)
}
failure(resolve, reject) {
return this.catch(reject)
}
}
let promise = new MyPromise(function(resolve, reject){
resolve(3)
})
promise.success(function(value){
console.log(value) // 3
}).failure(function(value){
console.log(value)
})
示例中的MyPromise派生自Promise,并扩展了success()方法和failure()方法。
由于静态方法也会被继承,所以MyPromise也有MyPromise.resolve()、MyPromise.reject()、MyPromise.race()和MyPromise. all() 这 4 个方法
异步
上一篇文章迭代器(Iterator)和生成器(Generator介绍了利用生成器完成异步任务执行
function run(task) {
let iterator = task(); // 生成器函数task的迭代器
let result = iterator.next(); // 启动任务
function step() { // 递归调用step函数,保持对next()的调用
if(!result.done) {
// 如果是函数,执行异步中的回调
if (typeof result.value === 'function') {
result.value(function(err, data) {
if(err) {
result = iterator.throw(err);
return;
}
result = iterator.next(data);
step();
})
}else{
result = iterator.next(result.value);
step();
}
}
}
step(); // 启动处理过程
}
function readFile(filename) {
return function(callback) {
fs.readFile(filename, callback)
}
}
run(function*() {
let contents = yield readFile('config.json');
doSomething(contents);
})
用这种方式实现异步任务有一些问题:首先,在返回值是函数的函数中包裹每一个函数会令人不易理解;其次,无法区分用作任务执行器回调函数的返回值和一个不是回调函数的返回值
如果每个异步操作都返回Promise,可以极大简化并通用化这个过程。
function run(task) {
let iterator = task(); // 生成器函数task的迭代器
let result = iterator.next(); // 启动任务
function step() { // 递归调用step函数,保持对next()的调用
if(!result.done) {
let promise = Promise.resolve(result.value);
promise.then(function(value){
result = iterator.next(value);
step();
}).catch(function(error){
result = iterator.throw(error);
step();
})
}
}
step(); // 启动处理过程
}
// 配合任务执行器的函数
function readFile(filename) {
return new Promise(function(resolve, reject){
// 异步任务
fs.readFile(filename, function(err, contents){
if(err){
reject(err)
}else{
resolve(contents)
}
})
})
}
// 运行任务
run(function *(){
let contents =yield readFile('config.josn')
doSomething(contents)
})
这个run()函数可以运行所有使用yield实现异步代码的生成器,而且不会将Promise或回调函数暴露给开发者。事实上,由于函数调用的返回值总会被转换成一个Promise,因此可以返回一些非Promise的值,也就是说,用yield调用同步或异步方法都可以正常运行,永远不需要检查返回值是否为Promise
尽管这种方式已经比较完美了,但是要写这么多代码还是挺麻烦的,ES7提供了async-await语法,现在已经是标准了,其基本思想是用async标记的函数代替生成器,用await代替yield来调用函数。下一篇介绍有关async-await的详细内容。