第十一章 Promise 与 异步编程
《深入理解ES6》—— Nicholas C. Zakas
JS能极其轻易地处理异步编程,如响应 点击或按键 之类的用户交互行为。
node.js 通过使用回调函数来代替事件,进一步推动了JS中的异步编程。
随着异步编程面对的场景日益复杂,事件与回调 已不足以支持所有需求,
Promise应运而生。
Promise 是异步编程的一种选择,延迟并在将来执行作业。
1. 异步编程的背景
1.1. 说明
JS引擎建立在单线程事件循环的概念上。
单线程:
单线程(single-threaded)意味着同一时刻只能执行一段代码。
多线程:
多段代码可以同时访问或修改状态,维护并保护这些状态就变成了难题。
作业队列(job queue)
每当一段代码准备被执行,它就会被添加到作业队列。
事件循环(event loop)
事件循环是JS引擎的一个内部处理线程,能监视代码的执行并管理作业队列。
当JS引擎结束当前代码的执行后,事件循环就会执行队列中的下一个作业。
1.2. 事件模型
当用户 点击一个按钮 时,一个事件(event)就被触发了,
该事件可能会对此交互进行响应,从而将一个新的作业添加到作业队列的尾部。
事件处理程序代码直到事件发生后才会被执行,此时它会拥有合适的上下文,例如:
let button = document.getElementById( "btn" );
button.onclick = handleClick;
function handleClick( event ) {
console.info( "clicked!" );
}
当 button
被点击,
赋值给 onclick
的函数就被添加到作业队列的尾部,
当队列前面的任务都结束都再执行。
优点:事件可以很好地工作于 简单的交互或类似的低频功能。
缺点:串联多个异步调用很麻烦,且要确保在事件触发之前绑定完毕。
1.3. 回调模式
当 node.js 发布时,它通过普及回调函数编程模式提升了异步编程模型。
回调函数模式类似于事件模型,因为异步代码也会在后面的一个时间点才执行。
不同之处在于需要调用的函数(即回调函数)是作为参数传入的,如下:
readFile( "foo.txt", function( err, contents ) {
if ( err ) { throw err; }
console.info( contents );
} );
使用回调函数模式,readFile()
会立即开始执行,并在开始读取磁盘时停止。
当 readFile()
结束操作后,它会将回调函数以及相关参数作为一个新的作业添加到作业队列的尾部。
之前的作业全部结束后,该作业才会执行。
回调函数模式要比事件模型灵活得多,因为使用回调函数串联多个调用会相对容易。
例如:
readFile( "foo.txt", function( err, contents ) {
if ( err ) { throw err; }
writeFile( "foo.txt", function( err ) {
if ( err ) { throw err; }
console.info( "File was written" );
} );
} );
这种模式运作得相当好,当嵌套过多回调函数时会陷入回调地狱(callback hell)。
在这些情况下,你需要追踪多个回调函数并做清理操作,Promise能大幅度改善这种情况。
2. Promise 基础
Promise是为 异步操作的结果 所准备的占位符。(异步操作的容器)
函数可以返回一个Promise,而不必订阅一个事件或向函数传递一个回调函数,
如下:
// readFile 承诺会在将来某个时间点完成
let promise = readFile( "foo.txt" );
readFile()
实际上并未立即开始读取文件。
此函数返回一个Promise对象以表示异步读取操作,因此你可以在将来再操作它。
2.1. Promise 的生命周期
Promise的生命周期:
- 尚未结束:挂起态(pending state),未决(unsettled)
- 操作结束:已决(settled)—— 完成(fulfilled),拒绝(rejected)
Promise的[[PromiseState]]
属性的值:
"pending"
"fulfilled"
"rejected"
Promise的then()
方法:
let promise = readFile( "foo.txt" );
promise.then(
contents => {
// 完成
console.info( contents );
},
err => {
// 拒绝
console.error( err.message );
}
);
promise.then( contents => {
// 完成
console.info( contents );
} );
promise.then( null, err => {
// 拒绝
console.error( err.message );
} );
Promise的catch()
方法,等同于只传拒绝处理函数给then()
:
promise.catch(
err => {
// 拒绝
console.error( err.message );
}
);
// 等同于:
promise.then( null, err => {
// 拒绝
console.error( err.message );
} )
then()
和catch()
的意图是让你组合使用它们来正确处理异步操作的结果。
此系统要优于事件与回调函数,因为它让操作是成功还是失败变得完全清晰。
事件模型倾向于在出错时不被触发(如点击按钮没反应),
而在回调函数模式中你必须始终记得检查错误参数。
2.2. 创建未决的Promise
let fs = require( "fs" );
function readFile( filename ) {
let executor = ( resolve, reject ) => {
fs.readFile( filename, { encoding: "utf8" }, ( err, contents ) => {
// 检查错误
if ( err ) {
reject( err );
return;
}
// 读取成功
resolve( contents );
} )
}
return new Promise( executor );
}
let promise = readFile( "foo.txt" );
promise.then( contents => {
// 完成
console.info( contents );
}, err => {
// 拒绝
console.error( err.message );
} );
fs.readFile()
异步调用被包裹在一个Promise中,
执行器会在readFile()
被调用时立即执行,
执行器要么传递错误对象给 reject()
函数,
要么传递文件内容给 resolve()
函数。
3. 串联 Promise
每次对 then()
或 catch()
的调用实际上创建并返回了另一个 Promise,
仅当前一个Promise被完成或拒绝时,后一个Promise才会被决议。
let p1 = new Promise( (resolve, reject) => {
resolve( "p1 resolved" );
//reject( "p1 rejected" );
} );
p1
.then(
value => {
console.info( value );
},
error => {
console.warn( "发生错误了!" );
}
)
.then(
() => {
console.info( "p1 完毕。" );
}
);
3.1. 捕获错误
Promise链允许你捕获前一个Promise的 完成或拒绝 处理函数中发生的错误。
例如:
let p1 = new Promise( (resolve, reject) => {
//resolve();
reject();
} );
p1
.then(
value => {
throw new Error( "resolve - Boom!" );
},
error => {
throw new Error( "reject - Boom!" );
}
)
.catch( error => {
console.warn( error.message );
} );
在Promise链尾部添加拒绝处理函数 以确保异常被处理。
3.2. 在Promise链中返回值
链中上一个Promise的 完成处理函数或拒绝处理函数 的非Promise返回值
会传递到下一个Promise的resolve。
let p1 = new Promise( (resolve, reject) => {
resolve(10); // 10 110
//reject(20); // 10 120
} );
p1
.then(
value => {
console.info( value );
return value + 100;
},
value => {
console.info( value );
return value + 200;
}
)
.then( value => {
console.info( value );
} );
3.3. 在Promise链中返回Promise
let p1 = new Promise( (resolve, reject) => {
resolve(1);
} );
p1
.then( value => {
console.info( value );
let p2 = new Promise( (resolve, reject) => {
resolve( 2 )
} )
return p2;
} )
.then( value => {
console.info( value );
let p3 = new Promise( (resolve, reject) => {
resolve( 3 )
} )
return p3;
} )
.then( value => {
console.info( value );
} )
.catch( error => {
console.info( error );
} );
4. 响应多个 Promise
ES6提供了能监视多个Promise的两个方法:
Promise.all()
、 Promise.race()
4.1. Promise.all()
每个Promise都完成,则结果为完成:
let p1 = new Promise( (resolve, reject) => {
resolve( "p1 - 完成" );
} );
let p2 = new Promise( (resolve, reject) => {
resolve( "p2 - 完成" );
} );
let p3 = Promise.all( [ p1, p2 ] );
p3.then( valueArray => {
console.info( valueArray );
//=> ["p1 - 完成", "p2 - 完成"]
} );
只要有一个Promise拒绝,则为拒绝:
let p1 = new Promise( (resolve, reject) => {
resolve( "p1 - 完成" );
} );
let p2 = new Promise( (resolve, reject) => {
reject( "p2 - 拒绝" );
} );
let p3 = Promise.all( [ p1, p2 ] );
p3.catch( rejectValue => {
console.info( rejectValue );
//=> "p2 - 拒绝"
} );
4.2. Promise.race()
首先被解决Promise为完成,则结果为完成:
let p1 = Promise.resolve( "p1 - 完成" );
let p2 = new Promise( (resolve, reject) => {
resolve( "p2 - 完成" );
} );
let p3 = Promise.race( [ p1, p2 ] );
p3.then( value => {
console.info( value );
//=> "p1 - 完成"
} )
首先被解决Promise为拒绝,则结果为拒绝:
let p1 = Promise.reject( "p1 - 拒绝" );
let p2 = new Promise( (resolve, reject) => {
resolve( "p2 - 完成" );
} );
let p3 = Promise.race( [ p1, p2 ] );
p3.catch( value => {
console.info( value );
//=> "p1 - 拒绝"
} )
5. 继承 Promise
继承Promise,对常规的 then()
catch
进行扩展,如下:
class MyPromise extends Promise {
done( resolve, reject ) {
return this.then( resolve, reject );
}
fail( reject ) {
return this.catch( reject );
}
}
6. 总结
Promise 有三种状态:挂起、已完成、已拒绝。
then()
方法允许绑定 完成处理函数和拒绝处理函数。
catch()
方法只允许绑定 拒绝处理函数,可置于链尾。