原文地址: http://www.2ality.com/2014/10/es6-promises-api.html
原文作者:Dr. Axel Rauschmayer
译者:倪颖峰
原博客已经标明:本博客文档已经过时,可进一步阅读“Exploring ES6”中的 “Promises for asynchronous programming”。仔细对比了下,两者的确存在一些差异。本文是在原来的译文基础上修订的。
(文章第二部分实在是太长,所以在此分成两部分翻译)
本文是通过普通的 promises 和 ES6的 promise API 来介绍异步编程。这是两篇系列文章的第二部分 - 第一部分介绍了一下异步编程的基础(你需要充分理解一下以便明白这篇文章)。
鉴于 ES6 的 promise API 比较容易从 ES5 标准模拟生成过来,所以这边主要使用函数表达式,而不使用更为简洁的 ES6 中的箭头函数。
下面函数通过一个Promise异步返回结果:
function asyncFunc() { return new Promise( function (resolve, reject) { resolve(value); // success ··· reject(error); // failure });}
可以像下面这样来调用asyncFunc()
:
asyncFunc() .then(value => { /* success */ }) .catch(error => { /* failure */ });
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` });
Promises 是一种来解决特定异步编程的模式:函数(或者方法)异步返回其结果。实现一个对返回结果的具有占位符意义的对象 promise。函数的调用者注册回调函数,一旦结果运算完毕就立即通知 promise。函数会由 promise 来传递结果。
JavaScript 的 promises 事实标准称为 Promises/A+。ES6 的 promise API 便遵循这个标准。
看一下第一个实例,来了解下 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()函数的代码在后面实现。
从生成者和消耗者两方面来看一下 promises 是如何操作的。
作为一个生成者,你创建一个 promise 然后用它传递结果:
let promise = new Promise( function(resolve, reject){ // (A) ... if( ... ){ resolve( value ); } else { reject( reason ); } });
一个 promise 一般处于以下三个(互斥)状态中的某一个状态:
Pending:还没有得到结果,进行中
Fulfilled:成功得到结果,通过
Rejected:在计算过程中发生一个错误,拒绝
一个 promise 稳定后(settled:代表运算已经完成 ),它要么是 fulfilled 要么是 rejected。每一个 promise 只能稳定一次,然后保持稳定状态。之后再处理它将不会触发任何效果。
new Promise() 的参数( 在 A 处开始的 )称为 executor(执行器):
1. 如果运算成功,执行器会将结果传递给 resolve()。这表示 promise Fulfilled(后面将会解释,如果你处理一个 promise 可能会不同)。
2. 如果错误发生了,执行器就会通过 reject() 通知 promise 消费者。就会拒绝该 promise。
作为 promise 的消费者,你会通过reactions - 利用 then 方法注册的回调函数,得到promise通过或者拒绝的通知 。
promise.then( function( value ){/* fulfillment */}, function( reason ){/* rejection */} );
正是由于一个 promise 一旦稳定之后再也不能变化,使得 promises 对于异步函数来说非常有用(一次性使用结果)。此外,还没有任何竞争条件,因为不论 promise是在稳定前还是在稳定后调用 then( ) 都是一样的:
1. 如果是在Promise稳定前注册的reactions, 那么一旦稳定将得到通知。
2. 如果是在Promise稳定后注册的reactions, 那么会立即收到缓存的稳定时的值(像任务那样排队被激活)。
如果你只关心通过状态,你可以忽略 then() 的第二个参数:
promise.then( function( value ){/* fulfillment */} );
如果你只对拒绝状态感兴趣,可以忽略第一个参数。用 catch() 方法也可以更紧凑的来实现。
promise.then( null, function( reason ){/* rejection */} ); // 等价于 promise.catch( function( reason ){/* rejection */} );
这里推荐只是用 then() 处理成功状态,使用 catch() 处理错误,因为这样可以更加优雅的标记回调函数,并且可以同时处理多个 promises 的拒绝状态(稍后解释)。
在深入探究promises之前,先使用前面学到的知识来看一些例子。
下面代码是将Node.js中的函数fs.readFile()利用
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); });
下面是一个基于 XMLHttpRequest API 事件,通过 promise 编写的 HTTP GET函数。
function httpGet( url ){ return new Promise( function( resolve, reject ){ var 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(){ console.log('contents: '+ value); }, function( reason ){ console.log('something error', reason); });
使用 setTimeout() 来实现基于 promise 的 delay()(类似于Q.delay())。
function delay( ms ){ return new Promise(function( resolve, reject ){ sertTimeout( resolve, ms ); // (A) }); } // 使用 delay() delay( 5000 ).then(function(){ // (B) console.log('5s have passed'); });
注意 A 处我们调用 resolve 没有传递参数,相当于我们这么调用 resolve( undefined )。在 B 处我们不需要通过的返回值,就可以简单的忽略它。仅仅通知就已经OK了。
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 处的超时失败并不会阻止这个请求,但是可以确保 promise 的成功状态结果。
如下方式使用 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); });
方法调用结果 P.then( onFulfilled, onRejected ) 是一个新的 promise Q。这意味着在 Q 中,你可以通过调用 then() 来保持对基于 promise 流的控制:
1. Q 在 onFulfilled 或者 onRejected 返回结果的时候,即为通过。
2. Q 在 onFulfilled 或者 onRejected 抛出错误的时候,即为拒绝。
如果你通过使用 then() 返回一个一般值来处理一个 Q,那你就可以通过下一个 then 来取到这个值:
asyncFunc().then( function(){ return 123; } ).then( function( value ){ console.log( value ); // 123 } )
你也可以通过 then() 来返回一个 then式的 R对象 来将 promise Q 通过。then式表示有 promise 风格方法 then() 的任何对象。即, promises 便是 then式的。处理 R (例如将其以 onFulfilled 来返回)意味着他是被插入到 Q 之后的:R的结果将被作为 Q onFulfilled 或者 onRejected 的回调函数。也就是说 Q 转变为 R。
这个形式主要是用来扁平化嵌套式调用 then(),比如下面的例子:
asyncFunc1() .then(function(value1){ asyncFunc2() .then(function(value2){ ... }); });
那么扁平化形式可以变为这样:
asyncFunc1() .then(function(value1){ return asyncFunc2(); }) .then(function(value2){ ... });
onRejected
如之前提到的,不管你在错误中返回什么都讲成为一个 fulfillment 的值(或者 rejection 值)。这使得你可以定义失败执行的默认值:
retrieveFileName() .catch(function(){ return 'Untitled.txt'; }).then(function(fileName){ ... });
作为异常,会将其作为then的一个参数抛出:
asyncFunc() .then(funcrion(value){ throw new Error(); }) .catch(function(reason){ // Handle error here });
执行过程中的异常将会传递到下一个错误处理中:
new Promise(function(resolve, reject){ throw new Error(); }) .catch(function(err){ // Handle error here });
将会有一个或者多个的 then() 调用但并没有提供一个错误处理。那么直到出现错误处理该错误才会被传递出来:
asyncFunc1() .then(asyncFunc2) .then(asyncFunc3) .catch(function(reason{ // something went wrong above });
本段会描述你如何将先现有的 promises 来组合创建新的 promise。我们已经使用过一种组合 promise 的方式了:通过 then() 连续的链式调用。Promise.all() 和 Promise.race() 提供了另一些组合的形式。
庆幸的是,基于 promise 返回结果,很多同步的工具仍然可以使用。比如你可以使用数组的方法 map():
var fileUrls = [ 'http://example.com/file1.txt', 'http://example.com/file2.txt' ]; var promisedTexts = fileUrls.map(httpGet);
promisedTexts 为一个 promises 对象的数组。Promise.all() 可以处理一个 promises 对象的数组(then式 和 其他值可以通过 Promise.resolve() 来转换为 promises)一旦所有的项状态都为 fulfilled,就会处理一个他们值得数组:
Promise.all(promisedTexts) .then(function(texts){ texts.forEach(function(text){ console.log(text); }); }) .catch(function(reason){ // 接受 promises 中第一个 rejection 状态 });
Promise.race() 使一个 promises 对象数组(then式 和 其他值可以通过 Promise.resolve() 来转换为 promises)返回一个 promise 对象 P。输入第一个的 promises 会将其结果通过参数传递给输出的 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){ ... });
一个 promise 库,不管结果是同步(正常方式)还是异步(当前延续的代码块完成之后)进行的传递都做好控制。然而,Promises/A+ 规范约定后一种方式应总是可用的。它以以下的条件来将状态传给 then():
当执行上下文栈中只包含平台代码时才执行 onFulfilled 或者 onRejected。
这意味着你可以依赖运行到完成的语态(第一部分中提到的),使得链式的 promises 不会使其他任务在闲置时处于等待状态。