原文地址: 原文作者:Dr. Axel Rauschmayer译者:倪颖峰
原博客已经标明:本博客文档已经过时,可进一步阅读“Exploring ES6”中的 “”。仔细对比了下,两者的确存在一些差异。本文是在原来的译文基础上修订的。
(文章第二部分实在是太长,所以在此分成两部分翻译)
本文是通过普通的Promises 和 ES6的 Promise API 来介绍异步编程。这是两篇系列文章的第二部分 - 第一部分介绍了一下异步编程的基础(你需要充分理解一下以便明白这篇文章)。
1. 概述
下面函数通过一个Promise异步返回结果:
function asyncFunc() { return new Promise( function (resolve, reject) { resolve(value); // success ··· reject(error); // failure });}
可以像下面这样来调用asyncFunc()
:
asyncFunc().then(value => { /* success */ }).catch(error => { /* failure */ });
1.1 处理Promises数组
Promise.all()可以遍历一个
Promises数组。
例如,可以通过数组方法map( )来创建一个Promises数组:
let fileUrls = [ 'http://example.com/file1.txt', 'http://example.com/file2.txt'];let promisedTexts = fileUrls.map(httpGet); // Array of Promises
如果对该数组应用Promise.all( )
,那么当所有的Promises被填充后将得到一个数组:
Promise.all(promisedTexts)// Success.then(texts => { for (let text of texts) { console.log(text); }})// Failure.catch(reason => { // Receives first rejection among `promisedTexts`});
2. Promises
Promises 是一种解决特定异步编程的模式:函数(或者方法)异步返回其结果。为实现该功能,返回结果为具有占位符意义的一个对象Promise。函数的调用者注册回调函数,一旦结果运算完毕就立即通知Promise。函数会由Promise 来传递结果。 JavaScript 的Promises 事实标准称为 Promises/A+。ES6 的Promise API 便遵循这个标准。
3. 第一个实例
看一下第一个实例,来了解下 Promises 是如何运行的。 NodeJS 风格的回调函数,异步读取文件如下所示:
fs.readFile('config.json', function (error, text) { if (error) { console.error('Error while reading config file'); } else { try { let obj = JSON.parse(text); console.log(JSON.stringify(obj, null, 4)); } catch (e) { console.error('Invalid JSON in file'); } }});
使用Promises,相同功能的实现可以是这样:
readFilePromisified('config.json').then(function (text) { // (A) let obj = JSON.parse(text); console.log(JSON.stringify(obj, null, 4));}).catch(function (reason) { // (B) // File read error or JSON SyntaxError console.error('An error occurred', reason);});
这里依旧是有回调函数,但这里是通过方法来提供的,是在有结果时(then() 和 catch())被调用的。在 B 处的报错的回调函数有两方面的优势:第一,这是一种单一风格的错误处理(与前一个例子中if(error) 和try-catch代码对比下)。 第二,你可以一个代码地点同时处理 readFilePromisified() 的错误 和 A 处回调函数的错误。
readFilePromisified()函数的实现代码见后面。
4. Promises 的创建和使用
从生成者和消耗者两方面来看一下Promises 是如何操作的。
4.1. 生成一个Promise
作为一个生成者,你创建一个Promise 然后用它传递结果:
let promise = new Promise( function(resolve, reject){ // (A) ... if( ... ){ resolve( value ); } else { reject( reason ); }});
一个Promise 一般处于以下三个(互斥)状态中的某一个状态:
Pending:还没有计算出结果
Fulfilled:成功计算出结果 Rejected:在计算过程中发生一个错误 一个Promise 被设置后(settled:代表运算已经完成 ),它的状态要么是 fulfilled 要么是 rejected。每一个 Promise 只能设置一次,然后保持settled状态。之后再设置它将不起作用。 new Promise() 的参数( 在 A 处开始的 )称为 executor(执行器): 1. 如果运算成功,执行器会通过resolve()传递结果,这通常会fufill Promise(后面将会解释,如果resolve的是一个 Promise可能会不同,见后面解释)。 2. 如果错误发生了,执行器就会通过 reject() 通知Promise 消费者,就会reject Promise。4.2. 使用Promise
作为Promise 的消费者,你会通过reactions - 利用 then( )方法注册的回调函数,得到fufillment或者rejection的通知 。
promise.then( function( value ){/* fulfillment */}, function( reason ){/* rejection */} );
正是由于一个Promise 一旦被设置后(settled)再也不能变化,使得Promises 对于异步函数来说非常有用(一次性使用结果)。此外,永远不会有任何竞争条件,因为不论Promise是在设置前还是在设置后调用 then( ) 都是一样的:
1. 如果是在Promise设置前注册的reactions, 那么一旦设置将得到通知。
2. 如果是在Promise设置后注册的reactions, 那么会立即收到缓存的设置的值(像任务那样排队被激活)。4.3. 只处理fullfillment或者rejection
如果你只关心fullfillment,你可以忽略 then() 的第二个参数:
promise.then( function( value ){/* fulfillment */});
如果你只对rejection感兴趣,可以忽略第一个参数。也可以用更紧凑的 catch() 方法来实现。
promise.then( null, function( reason ){/* rejection */} );// 等价于promise.catch( function( reason ){/* rejection */} );
这里推荐只用 then() 处理fullfillment,使用 catch() 处理错误,因为这样可以更加优雅的标记回调函数,并且可以同时处理多个Promises 的rejection(稍后解释)。
5. 举例
在深入探究Promises之前,先使用前面学到的知识来看一些例子。
5.1 举例:fs.readFile() Promise化
下面代码是将Node.js中的函数利用
Promise来重写:
import {readFile} from 'fs';function readFilePromisified(filename) { return new Promise( function (resolve, reject) { readFile(filename, { encoding: 'utf8' }, (error, data) => { if (error) { reject(error); } else { resolve(data); } }); });}
readFilePromisified()的调用如下:
readFilePromisified(process.argv[2]).then(text => { console.log(text);}).catch(error => { console.log(error);});
5.2 举例:XMLHttpRequest promise化
下面是一个基于 XMLHttpRequest API 事件,通过Promise 编写的 HTTP GET函数。
function httpGet( url ){ return new Promise( function( resolve, reject ){ let request = new XMLHttpRequest(); request.onreadystatechange = function(){ if( this.status === 200 ){ // success resolve( this.response ); }else { reject( new Error( this.statusText ) ); } } request.onerror = function(){ reject( new Error('XMLHttpRequest Error: ' + this.statusText ) ); }; request.open( 'GET', url ); request.send(); });}
下面是如何使用 httpGet():
httpGet("http://example.com/file.txt").then( function (value){ console.log('contents: '+ value); }, function (reason){ console.log('something error', reason); });
5.3 举例:延迟活动
使用Promise 的 delay( )函数(类似于Q.delay())来实现 setTimeout( ) 。
function delay (ms){ return new Promise(function(resolve, reject){ setTimeout(resolve, ms); // (A) });}// 使用 delay()delay(5000).then(function(){ // (B) console.log('5s have passed');});
注意 A 处我们调用 resolve 没有传递参数,相当于调用了resolve( undefined )。在 B 处我们不需要通过的返回值,就可以简单的忽略它。仅仅通知就已经OK了。
5.4 举例:Promise超时
function timeout(ms, promise){ return new Promise(function(resolve, reject){ promise.then( resolve ); setTimeout(function(){ reject(new Error('Timeout after ' + ms + ' ms')); // (A) }, ms) });}
注意在 A 处超时后rejection并不会取消这个请求,但是会阻止Promise 达到fulfilled状态。 如下方式使用 timeout():
timeout(5000, httpGet("http://example.com/file.txt") ).then(function(value){ console.log('contents: ' + value);}).catch(function(reason){ console.log('error or timeout: ' , reason);});
6. 链式调用 then()
调用 P.then( onFulfilled, onRejected ) 会得到一个新的Promise Q。这意味着在 Q 中,你可以通过调用 then() 来保持对Promise的控制流: 1. Q 在 onFulfilled 或者 onRejected 返回结果的时候,即为resolved。 2. Q 在 onFulfilled 或者 onRejected 抛出异常的时候,即为rejected。
6.1. Resolving Q返回一般值
如果then() 返回的是个一般值,那么可以resolve这个 Promise Q,你可以在下一个 then( ) 取到这个值:
asyncFunc().then(function(){ return 123;}).then(function(value){ console.log(value); // 123})
6.2. Resolving Q返回then对象(thenables)
如果then() 返回的是个 then对象R,那么也可以resolve这个Promise Q。then对象(thenable)表示有Promise 风格 then() 方法的任何对象,因此Promises就是then对象。resolve R (例如通过onFulfilled返回)意味着它被插入到 Q 之后:R的settlement将被传递给 Q的onFulfilled 或者 onRejected回调函数。也就是说 Q 转变成了 R。 这个形式主要是用来扁平化嵌套式调用 then(),比如下面的例子:
asyncFunc1().then(function(value1){ asyncFunc2() .then(function(value2){ ... });});
那么扁平化形式可以变为这样:
asyncFunc1().then(function(value1){ return asyncFunc2();}).then(function(value2){ ...});
6.3 Resolving Q from onRejected
如之前提到的,不管你在错误handler中返回什么,都将成为一个 fulfillment 的值(注意不是rejection 值)。这使得你可以定义失败情况下用到默认值:
retrieveFileName().catch(function(){ // Something went wrong, use a default value return 'Untitled.txt';}).then(function(fileName){ ...});
6.4 抛出异常拒绝Q(Rejecting Q by throwing exceptions)
从then的任意一个参数抛出的异常被传递给下一个错误handler:
asyncFunc().then(funcrion(value){ throw new Error();}).catch(function(reason){ // Handle error here});
6.5 执行过程中的异常(Exceptions in executors)
executor(new Promise()的回调函数)中抛出的异常,将会传递到由executor管理的Promise的错误handler中:
new Promise(function(resolve, reject){ throw new Error();}).catch(function(err){ // Handle error here});
6.6 链式的错误处理
会有一个或多个 then() 调用没有提供错误handler,那么直到出现错误handler该错误才会被传递进去:
asyncFunc1().then(asyncFunc2).then(asyncFunc3).catch(function(reason{ // something went wrong above});
7. 组合
本章节会描述你如何组合现有的Promises 来创建新的Promise。我们已经使用过一种组合Promise 的方式了:通过 then() 连续的链式调用。Promise.all() 和 Promise.race() 提供了另一些组合的形式。
7.1. 通过 Promise.all() 实现 map()
庆幸的是,基于Promise的函数可以返回结果,因此很多同步工具仍然可以使用。比如你可以使用数组的方法 map():
let fileUrls = [ 'http://example.com/file1.txt', 'http://example.com/file2.txt'];let promisedTexts = fileUrls.map(httpGet);
promisedTexts 是一个Promises 数组。Promise.all() 可以处理一个Promises 数组(then对象和 其他值可以通过 Promise.resolve() 来转换为 Promises),一旦所有的项状态都为 fulfilled,就会得到取值的数组:
Promise.all(promisedTexts).then(texts=> { for (let text of texts) { console.log(text); }}).catch(reason => { // Receives first rejection among the Promises});
7.2. 通过 Promise.race() 实现延时
Promise.race() 接受一个 Promises 数组(then对象和其他值可以通过 Promise.resolve() 来转换为 Promises),返回一个 Promise 对象 P。第一个传入的 Promise会将其settlement传递给输出的Promise。 举个例子,使用 Promise.race() 来实现一个 timeout:
Promise.race([ httpGet('http://example.com/file.txt'), delay(5000).then(function(){ throw new Error('Time out'); })]).then(function(text){ ... }).catch(function(reason){ ... });
8. Promises总是异步的
一个Promise 库,不管是同步(正常方式)还是异步(当前代码块之后继续执行直至完成)将结果送给Promise的reactions,都可以完全控制。然而,Promises/A+ 规范约定总是用后一种方式。它以then()方法的需求(2.2.4)来描述: 只有当执行上下文栈中仅仅包含平台代码时,才可以调用 onFulfilled 或者 onRejected。 这意味着你可以依赖运行到完成的语态(run-to-completion semantics:第一部分中提到的),使得链式的Promises 不会使其他任务没有时间得到处理。