配色: 字号:
JavaScript Promise:去而复返
2016-09-19 | 阅:  转:  |  分享 
  
JavaScriptPromise:去而复返

原文:http://www.html5rocks.com/en/tutorials/es6/promises/
作者:JakeArchibald
翻译:Amio

女士们先生们,请准备好迎接Web开发历史上一个重大时刻……

[鼓声响起]

JavaScript有了原生的Promise!

[漫天的烟花绽放,人群沸腾了]

这时候你大概是这三种人之一:

你的身边拥挤着欢呼的人群,但是你却不在其中,甚至你还不大清楚“Promise”是什么。你耸耸肩,烟花的碎屑在你的身边落下。这样的话,不要担心,我也是花了多年的时间才明白Promise的意义,你可以从入门简介:他们都在激动什么?开始看起。
你一挥拳!太赞了对么!你已经用过一些Promise的库,但是所有这些第三方实现在API上都略有差异,JavaScript官方的API会是什么样子?看这里:Promise术语!
你早就知道了,看着那些欢呼雀跃的新人你的嘴角泛起一丝不屑的微笑。你可以安静享受一会儿优越感,然后直接去看API参考吧。
他们都在激动什么?

JavaScript是单线程的,这意味着任何两句代码都不能同时运行,它们得一个接一个来。在浏览器中,JavaScript和其他任务共享一个线程,不同的浏览器略有差异,但大体上这些和JavaScript共享线程的任务包括重绘、更新样式、用户交互等,所有这些任务操作都会阻塞其他任务。

作为人类,你是多线程的。你可以用多个手指同时敲键盘,也可以一边开车一遍电话。唯一的全局阻塞函数是打喷嚏,打喷嚏期间所有其他事务都会暂停。很烦人对么?尤其是当你开着车打着电话的时候。我们都不喜欢这样打喷嚏的代码。

你应该会用事件加回调的办法来处理这类情况:

varimg1=document.querySelector(''.img-1'');

img1.addEventListener(''load'',function(){
//wooyeyimageloaded
});

img1.addEventListener(''error'',function(){
//argheverything''sbroken
});
这样就不打喷嚏了。我们添加几个监听函数,请求图片,然后JavaScript就停止运行了,直到触发某个监听函数。

上面的例子中唯一的问题是,事件有可能在我们绑定监听器之前就已经发生,所以我们先要检查图片的complete属性:

varimg1=document.querySelector(''.img-1'');

functionloaded(){
//wooyeyimageloaded
}

if(img1.complete){
loaded();
}
else{
img1.addEventListener(''load'',loaded);
}

img1.addEventListener(''error'',function(){
//argheverything''sbroken
});
这样还不够,如果在添加监听函数之前图片加载发生错误,我们的监听函数还是白费,不幸的是DOM也没有为这个需求提供解决办法。而且,这还只是处理一张图片的情况,如果有一堆图片要处理那就更麻烦了。

事件不是万金油

事件机制最适合处理同一个对象上反复发生的事情——keyup、touchstart等等。你不需要考虑当绑定监听器之前所发生的事情,当碰到异步请求成功/失败的时候,你想要的通常是这样:

img1.callThisIfLoadedOrWhenLoaded(function(){
//loaded
}).orIfFailedCallThis(function(){
//failed
});

//and…
whenAllTheseHaveLoaded([img1,img2]).callThis(function(){
//allloaded
}).orIfSomeFailedCallThis(function(){
//oneormorefailed
});
这就是Promise。如果HTML图片元素有一个ready()方法的话,我们就可以这样:

img1.ready().then(function(){
//loaded
},function(){
//failed
});

//and…
Promise.all([img1.ready(),img2.ready()]).then(function(){
//allloaded
},function(){
//oneormorefailed
});
基本上Promise还是有点像事件回调的,除了:

一个Promise只能成功或失败一次,并且状态无法改变(不能从成功变为失败,反之亦然)
如果一个Promise成功或者失败之后,你为其添加针对成功/失败的回调,则相应的回调函数会立即执行
这些特性非常适合处理异步操作的成功/失败情景,你无需再担心事件发生的时间点,而只需对其做出响应。

Promise相关术语

DomenicDenicola审阅了本文初稿,给我在术语方面打了个“F”,关了禁闭并且责令我打印StatesandFates一百遍,还写了一封家长信给我父母。即便如此,我还是对术语有些迷糊,不过基本上应该是这样:

一个Promise的状态可以是:

确认(fulfilled)-成功了
否定(rejected)-失败了
等待(pending)-还没有确认或者否定,进行中
结束(settled)-已经确认或者否定了

规范里还使用“thenable”来描述一个对象是否是“类Promise”(拥有名为“then”的方法)的。这个术语使我想起来前英格兰足球经理TerryVenables所以我尽量少用它。

JavaScript有了Promise!

其实已经有一些第三方库实现了Promise:

Q
when
WinJS
RSVP.js
上面这些库和JavaScript原生Promise都遵守一个通用的、标准化的规范:Promises/A+,jQuery有个类似的方法叫Deferreds,但不兼容Promises/A+规范,于是会有点小问题,使用需谨慎。jQuery还有一个Promise类型,但只是Deferreds的缩减版,所以也有同样问题。

尽管Promise的各路实现遵循同一规范,它们的API还是各不相同。JavaScriptPromise的API比较接近RSVP.js,如下创建Promise:

varpromise=newPromise(function(resolve,reject){
//doathing,possiblyasync,then…

if(/everythingturnedoutfine/){
resolve("Stuffworked!");
}
else{
reject(Error("Itbroke"));
}
});
Promise的构造器接受一个函数作为参数,它会传递给这个回调函数两个变量resolve和reject。在回调函数中做一些异步操作,成功之后调用resolve,否则调用reject。

调用reject的时候传递给它一个Error对象只是个惯例并非必须,这和经典JavaScript中的throw一样。传递Error对象的好处是它包含了调用堆栈,在调试的时候会有点用处。

现在来看看如何使用Promise:

promise.then(function(result){
console.log(result);//"Stuffworked!"
},function(err){
console.log(err);//Error:"Itbroke"
});
then接受两个参数,成功的时候调用一个,失败的时候调用另一个,两个都是可选的,所以你可以只处理成功的情况或者失败的情况。

JavaScriptPromise最初以“Futures”的名称归为DOM规范,后来改名为“Promises”,最终纳入JavaScript规范。将其加入JavaScript而非DOM的好处是方便在非浏览器环境中使用,如Node.js(他们会不会在核心API中使用就是另一回事了)。

浏览器支持和Polyfill

目前的浏览器已经(部分)实现了Promise。

用Chrome的话,就像个Chroman一样装上Canary版,默认即启用了Promise支持。如果是Firefox拥趸,安装最新的nightlybuild也一样。

不过这两个浏览器的实现都还不够完整彻底,你可以在bugzilla上跟踪Firefox的最新进展或者到ChromiumDashboard查看Chrome的实现情况。

要在这两个浏览器上达到兼容标准Promise,或者在其他浏览器以及Node.js中使用Promise,可以看看这个polyfill(gzip之后2K)

与其他库的兼容性

JavaScriptPromise的API会把任何包含有then方法的对象当作“类Promise”(或者用术语来说就是thenable。叹气)的对象,这些对象经过Promise.cast()处理之后就和原生的JavaScriptPromise实例没有任何区别了。所以如果你使用的库返回一个QPromise,那没问题,无缝融入新的JavaScriptPromise。

尽管,如前所述,jQuery的Deferred对象有点……没什么用,不过幸好还可以转换成标准Promise,你最好一拿到对象就马上加以转换:

varjsPromise=Promise.cast($.ajax(''/whatever.json''));
这里jQuery的$.ajax返回一个Deferred对象,含有then方法,因此Promise.cast可以将其转换为JavaScriptPromise。不过有时候Deferred对象会给它的回调函数传递多个参数,例如:

varjqDeferred=$.ajax(''/whatever.json'');

jqDeferred.then(function(response,statusText,xhrObj){
//...
},function(xhrObj,textStatus,err){
//...
});
除了第一个参数,其他都会被JavaScriptPromise忽略掉:

jsPromise.then(function(response){
//...
},function(xhrObj){
//...
});
……还好这通常就是你想要的了,至少你能够用这个办法实现想要的。另外还要注意,jQuery也没有遵循给否定回调函数传递Error对象的惯例。

复杂的异步代码变得更简单了

OK,现在我们来写点实际的代码。假设我们想要:

显示一个加载指示图标
加载一篇小说的JSON,包含小说名和每一章内容的URL。
在页面中填上小说名
加载所有章节正文
在页面中添加章节正文
停止加载指示
……这个过程中如果发生什么错误了要通知用户,并且把加载指示停掉,不然它就会不停转下去,令人眼晕,或者搞坏界面什么的。

当然了,你不会用JavaScript去这么繁琐地显示一篇文章,直接输出HTML要快得多,不过这个流程是非常典型的API请求模式:获取多个数据,当它们全部完成之后再做一些事情。

首先搞定从网络加载数据的步骤:

将Promise用于XMLHttpRequest

只要能保持向后兼容,现有API都会更新以支持Promise,XMLHttpRequest是重点考虑对象之一。不过现在我们先来写个GET请求:

functionget(url){
//Returnanewpromise.
returnnewPromise(function(resolve,reject){
//DotheusualXHRstuff
varreq=newXMLHttpRequest();
req.open(''GET'',url);

req.onload=function(){
//Thisiscalledevenon404etc
//socheckthestatus
if(req.status==200){
//Resolvethepromisewiththeresponsetext
resolve(req.response);
}
else{
//Otherwiserejectwiththestatustext
//whichwillhopefullybeameaningfulerror
reject(Error(req.statusText));
}
};

//Handlenetworkerrors
req.onerror=function(){
reject(Error("NetworkError"));
};

//Maketherequest
req.send();
});
}
然后调用它:

get(''story.json'').then(function(response){
console.log("Success!",response);
},function(error){
console.error("Failed!",error);
});
点击这里查看代码运行页面,打开控制台查看输出结果。现在我们可以直接发起HTTP请求而不需要手敲XMLHttpRequest,这样感觉好多了,能少看一次这个狂驼峰命名的XMLHttpRequest我就多快乐一点。

链式调用

“then”的故事还没完,你可以把这些“then”串联起来修改结果或者添加进行更多异步操作。

值的处理

你可以对结果做些修改然后返回一个新值:

varpromise=newPromise(function(resolve,reject){
resolve(1);
});

promise.then(function(val){
console.log(val);//1
returnval+2;
}).then(function(val){
console.log(val);//3
});
回到前面的代码:

get(''story.json'').then(function(response){
console.log("Success!",response);
});
收到的响应是一个纯文本的JSON,我们可以修改get函数,设置responseType要求服务器以JSON格式提供响应,不过还是用Promise的方式来搞定吧:

get(''story.json'').then(function(response){
returnJSON.parse(response);
}).then(function(response){
console.log("YeyJSON!",response);
});
既然JSON.parse只接收一个参数,并返回转换后的结果,我们还可以再精简一点:

get(''story.json'').then(JSON.parse).then(function(response){
console.log("YeyJSON!",response);
});
点击这里查看代码运行页面,打开控制台查看输出结果。事实上,我们可以把getJSON函数写得超级简单:

functiongetJSON(url){
returnget(url).then(JSON.parse);
}
getJSON会返回一个获取JSON并加以解析的Promise。

队列的异步操作

你也可以把then串联起来依次执行异步操作。

当你从then的回调函数返回的时候,这里有点小魔法。如果你返回一个值,它就会被传给下一个then的回调;而如果你返回一个“类Promise”的对象,则下一个then就会等待这个Promise明确结束(成功/失败)才会执行。例如:

getJSON(''story.json'').then(function(story){
returngetJSON(story.chapterUrls[0]);
}).then(function(chapter1){
console.log("Gotchapter1!",chapter1);
});
这里我们发起一个对story.json的异步请求,返回给我们更多URL,然后我们会请求其中的第一个。Promise开始首次显现出相较事件回调的优越性了。你甚至可以写一个抓取章节内容的独立函数:

varstoryPromise;

functiongetChapter(i){
storyPromise=storyPromise||getJSON(''story.json'');

returnstoryPromise.then(function(story){
returngetJSON(story.chapterUrls[i]);
})
}

//andusingitissimple:
getChapter(0).then(function(chapter){
console.log(chapter);
returngetChapter(1);
}).then(function(chapter){
console.log(chapter);
});
我们一开始并不加载story.json,直到第一次getChapter,而以后每次getChapter的时候都可以重用已经加载完成的storyPromise,所以story.json只需要请求一次。Promise好棒!

错误处理

前面已经看到,“then”接受两个参数,一个处理成功,一个处理失败(或者说确认和否定,按Promise术语):

get(''story.json'').then(function(response){
console.log("Success!",response);
},function(error){
console.log("Failed!",error);
});
你还可以使用catch:

get(''story.json'').then(function(response){
console.log("Success!",response);
}).catch(function(error){
console.log("Failed!",error);
});
这里的catch并无任何特殊之处,只是then(undefined,func)的语法糖衣,更直观一点而已。注意上面两段代码的行为不仅相同,后者相当于:

get(''story.json'').then(function(response){
console.log("Success!",response);
}).then(undefined,function(error){
console.log("Failed!",error);
});
差异不大,但意义非凡。Promise被否定之后会跳转到之后第一个配置了否定回调的then(或catch,一样的)。对于then(func1,func2)来说,必会调用func1或func2之一,但绝不会两个都调用。而then(func1).catch(func2)这样,如果func1返回否定的话func2也会被调用,因为他们是链式调用中独立的两个步骤。看下面这段代码:

asyncThing1().then(function(){
returnasyncThing2();
}).then(function(){
returnasyncThing3();
}).catch(function(err){
returnasyncRecovery1();
}).then(function(){
returnasyncThing4();
},function(err){
returnasyncRecovery2();
}).catch(function(err){
console.log("Don''tworryaboutit");
}).then(function(){
console.log("Alldone!");
});
这段流程非常像JavaScript的try/catch组合,try代码块中发生的错误会径直跳转到catch代码块。这是上面那段代码的流程图(我最爱流程图了):



绿线是确认的Promise流程,红线是否定的。

JavaScript异常和Promise

Promise的否定回调可以由Promise.reject()触发,也可以由构造器回调中抛出的错误触发:

varjsonPromise=newPromise(function(resolve,reject){
//JSON.parsethrowsanerrorifyoufeeditsome
//invalidJSON,sothisimplicitlyrejects:
resolve(JSON.parse("Thisain''tJSON"));
});

jsonPromise.then(function(data){
//Thisneverhappens:
console.log("Itworked!",data);
}).catch(function(err){
//Instead,thishappens:
console.log("Itfailed!",err);
});
这意味着你可以把所有Promise相关工作都放在构造函数的回调中进行,这样任何错误都能被捕捉到并且触发Promise否定。

get(''/'').then(JSON.parse).then(function(){
//Thisneverhappens,''/''isanHTMLpage,notJSON
//soJSON.parsethrows
console.log("Itworked!",data);
}).catch(function(err){
//Instead,thishappens:
console.log("Itfailed!",err);
});
实践错误处理

回到我们的故事和章节,我们用catch来捕捉错误并显示给用户:

getJSON(''story.json'').then(function(story){
returngetJSON(story.chapterUrls[0]);
}).then(function(chapter1){
addHtmlToPage(chapter1.html);
}).catch(function(){
addTextToPage("Failedtoshowchapter");
}).then(function(){
document.querySelector(''.spinner'').style.display=''none'';
});
如果请求story.chapterUrls[0]失败(http500或者用户掉线什么的)了,它会跳过之后所有针对成功的回调,包括getJSON中将响应解析为JSON的回调,和这里把第一张内容添加到页面里的回调。JavaScript的执行会进入catch回调。结果就是前面任何章节请求出错,页面上都会显示“Failedtoshowchapter”。

和JavaScript的catch一样,捕捉到错误之后,接下来的代码会继续执行,按计划把加载指示器给停掉。上面的代码就是下面这段的非阻塞异步版:

try{
varstory=getJSONSync(''story.json'');
varchapter1=getJSONSync(story.chapterUrls[0]);
addHtmlToPage(chapter1.html);
}
catch(e){
addTextToPage("Failedtoshowchapter");
}

document.querySelector(''.spinner'').style.display=''none'';
如果只是要捕捉异常做记录输出,不打算在用户界面上对错误进行反馈的话,只要抛出Error就行了,这一步可以放在getJSON中:

functiongetJSON(url){
returnget(url).then(JSON.parse).catch(function(err){
console.log("getJSONfailedfor",url,err);
throwerr;
});
}
现在我们已经搞定第一章了,接下来搞定所有的。

并行和串行——鱼与熊掌兼得

异步的思维方式并不符合直觉,如果你觉得起步困难,那就试试先写个同步的方法,就像这个:

try{
varstory=getJSONSync(''story.json'');
addHtmlToPage(story.heading);

story.chapterUrls.forEach(function(chapterUrl){
varchapter=getJSONSync(chapterUrl);
addHtmlToPage(chapter.html);
});

addTextToPage("Alldone");
}
catch(err){
addTextToPage("Argh,broken:"+err.message);
}

document.querySelector(''.spinner'').style.display=''none'';
它执行起来完全正常!(查看示例)不过它是同步的,在加载内容时会卡住整个浏览器。要让它异步工作的话,我们用then把它们一个接一个串起来:

getJSON(''story.json'').then(function(story){
addHtmlToPage(story.heading);

//TODO:foreachurlinstory.chapterUrls,fetch&display
}).then(function(){
//Andwe''realldone!
addTextToPage("Alldone");
}).catch(function(err){
//Catchanyerrorthathappenedalongtheway
addTextToPage("Argh,broken:"+err.message);
}).then(function(){
//Alwayshidethespinner
document.querySelector(''.spinner'').style.display=''none'';
});
那么我们如何遍历章节的URL并且依次请求?这样是不行的:

story.chapterUrls.forEach(function(chapterUrl){
//Fetchchapter
getJSON(chapterUrl).then(function(chapter){
//andaddittothepage
addHtmlToPage(chapter.html);
});
});
forEach没有对异步操作的支持,所以我们的故事章节会按照它们加载完成的顺序显示,基本上《低俗小说》就是这么写出来的。我们不写低俗小说,所以得修正它:

创建序列

我们要把章节URL数组转换成Promise的序列,还是用then:

//Startoffwithapromisethatalwaysresolves
varsequence=Promise.resolve();

//Loopthroughourchapterurls
story.chapterUrls.forEach(function(chapterUrl){
//Addtheseactionstotheendofthesequence
sequence=sequence.then(function(){
returngetJSON(chapterUrl);
}).then(function(chapter){
addHtmlToPage(chapter.html);
});
});
这是我们第一次用到Promise.resolve,它会依据你传的任何值返回一个Promise。如果你传给它一个类Promise对象(带有then方法),它会生成一个带有同样确认/否定回调的Promise,基本上就是克隆。如果传给它任何别的值,如Promise.resolve(''Hello''),它会创建一个以这个值为完成结果的Promise,如果不传任何值,则以undefined为完成结果。

还有一个对应的Promise.reject(val),会创建以你传入的参数(或undefined)为否定结果的Promise。

我们可以用array.reduce精简一下上面的代码:

//Loopthroughourchapterurls
story.chapterUrls.reduce(function(sequence,chapterUrl){
//Addtheseactionstotheendofthesequence
returnsequence.then(function(){
returngetJSON(chapterUrl);
}).then(function(chapter){
addHtmlToPage(chapter.html);
});
},Promise.resolve());
它和前面的例子功能一样,但是不需要显式声明sequence变量。reduce回调会依次应用在每个数组元素上,第一轮里的“sequence”是Promise.resolve(),之后的调用里“sequence”就是上次函数执行的的结果。array.reduce非常适合用于把一组值归并处理为一个值,正是我们现在对Promise的用法。

汇总下上面的代码:

getJSON(''story.json'').then(function(story){
addHtmlToPage(story.heading);

returnstory.chapterUrls.reduce(function(sequence,chapterUrl){
//Oncethelastchapter''spromiseisdone…
returnsequence.then(function(){
//…fetchthenextchapter
returngetJSON(chapterUrl);
}).then(function(chapter){
//andaddittothepage
addHtmlToPage(chapter.html);
});
},Promise.resolve());
}).then(function(){
//Andwe''realldone!
addTextToPage("Alldone");
}).catch(function(err){
//Catchanyerrorthathappenedalongtheway
addTextToPage("Argh,broken:"+err.message);
}).then(function(){
//Alwayshidethespinner
document.querySelector(''.spinner'').style.display=''none'';
});
运行示例看这里,前面的同步代码改造成了完全异步的版本。我们还可以更进一步,现在页面加载的效果是这样:



浏览器很擅长同时加载多个文件,我们这种一个接一个下载章节的方法非常不效率。我们希望同时下载所有章节,全部完成后一次搞定,正好就有这么个API:

Promise.all(arrayOfPromises).then(function(arrayOfResults){
//...
});
Promise.all接受一个Promise数组为参数,创建一个当所有Promise都完成之后就完成的Promise,它的完成结果是一个数组,包含了所有先前传入的那些Promise的完成结果,顺序和将它们传入的数组顺序一致。

getJSON(''story.json'').then(function(story){
addHtmlToPage(story.heading);

//Takeanarrayofpromisesandwaitonthemall
returnPromise.all(
//Mapourarrayofchapterurlsto
//anarrayofchapterjsonpromises
story.chapterUrls.map(getJSON)
);
}).then(function(chapters){
//Nowwehavethechaptersjsonsinorder!Loopthrough…
chapters.forEach(function(chapter){
//…andaddtothepage
addHtmlToPage(chapter.html);
});
addTextToPage("Alldone");
}).catch(function(err){
//catchanyerrorthathappenedsofar
addTextToPage("Argh,broken:"+err.message);
}).then(function(){
document.querySelector(''.spinner'').style.display=''none'';
});
根据连接状况,改进的代码会比顺序加载方式提速数秒,甚至代码行数也更少。章节加载完成的顺序不确定,但它们显示在页面上的顺序准确无误。



然而这样还是有提高空间。当第一章内容加载完毕我们可以立即填进页面,这样用户可以在其他加载任务尚未完成之前就开始阅读;当第三章到达的时候我们不动声色,第二章也到达之后我们再把第二章和第三章内容填入页面,以此类推。

为了达到这样的效果,我们同时请求所有的章节内容,然后创建一个序列依次将其填入页面:

getJSON(''story.json'').then(function(story){
addHtmlToPage(story.heading);

//Mapourarrayofchapterurlsto
//anarrayofchapterjsonpromises.
//Thismakessuretheyalldownloadparallel.
returnstory.chapterUrls.map(getJSON)
.reduce(function(sequence,chapterPromise){
//Usereducetochainthepromisestogether,
//addingcontenttothepageforeachchapter
returnwww.wang027.comsequence.then(function(){
//Waitforeverythinginthesequencesofar,
//thenwaitforthischaptertoarrive.
returnchapterPromise;
}).then(function(chapter){
addHtmlToPage(chapter.html);
});
},Promise.resolve());
}).then(function(){
addTextToPage("Alldone");
}).catch(function(err){
//catchanyerrorthathappenedalongtheway
addTextToPage("Argh,broken:"+err.message);
}).then(function(){
document.querySelector(''.spinner'').style.display=''none'';
});
哈哈(查看示例),鱼与熊掌兼得!加载所有内容的时间未变,但用户可以更早看到第一章。



这个小例子中各部分章节加载差不多同时完成,逐章显示的策略在章节内容很多的时候优势将会更加显著。

上面的代码如果用Node.js风格的回调或者事件机制实现的话代码量大约要翻一倍,更重要的是可读性也不如此例。然而,Promise的厉害不止于此,和其他ES6的新功能结合起来还能更加高效……

附赠章节:Promise和Generator

接下来的内容涉及到一大堆ES6的新特性,不过对于现在应用Promise来说并非必须,把它当作接下来的第二部豪华续集的预告片来看就好了。

ES6还给我们带来了Generator,允许函数在特定地方像return一样退出,但是稍后又能恢复到这个位置和状态上继续执行。

functionaddGenerator(){
vari=0;
while(true){
i+=yieldi;
}
}
注意函数名前的星号,这表示该函数是一个Generator。关键字yield标记了暂停/继续的位置,使用方法像这样:

varadder=addGenerator();
adder.next().value;//0
adder.next(5).value;//5
adder.next(5).value;//10
adder.next(5).value;//15
adder.next(50).value;//65
这对Promise有什么用呢?你可以用这种暂停/继续的机制写出来和同步代码看上去差不多(理解起来也一样简单)的代码。下面是一个辅助函数(helperfunction),我们在yield位置等待Promise完成:

functionspawn(generatorFunc){
functioncontinuer(verb,arg){
varresult;
try{
result=generator[verb](arg);
}catch(err){
returnPromise.reject(err);
}
if(result.done){
returnresult.value;
}else{
returnPromise.cast(result.value).then(onFulfilled,onRejected);
}
}
vargenerator=generatorFunc();
varonFulfilled=continuer.bind(continuer,"next");
varonRejected=continuer.bind(continuer,"throw");
returnonFulfilled();
}
这段代码原样拷贝自Q,只是改成JavaScriptPromise的API。把我们前面的最终方案和ES6最新特性结合在一起之后:

spawn(function(){
try{
//''yield''effectivelydoesanasyncwait,
//returningtheresultofthepromise
letstory=yieldgetJSON(''story.json'');
addHtmlToPage(story.heading);

//Mapourarrayofchapterurlsto
//anarrayofchapterjsonpromises.
//Thismakessuretheyalldownloadparallel.
letchapterPromises=story.chapterUrls.map(getJSON);

for(letchapterPromiseofchapterPromises){
//Waitforeachchaptertobeready,thenaddittothepage
letchapter=yieldchapterPromise;
addHtmlToPage(chapter.html);
}

addTextToPage("Alldone");
}
catch(err){
//try/catchjustworks,rejectedpromisesarethrownhere
addTextToPage("Argh,broken:"+err.message);
}
document.querySelector(''.spinner'').style.display=''none'';
});
功能完全一样,读起来要简单得多。这个例子目前可以在ChromeCanary中运行(查看示例),不过你得先到about:flags中开启EnableexperimentalJavaScript选项。

这里用到了一堆ES6的新语法:Promise、Generator、let、for-of。当我们把yield应用在一个Promise上,spawn辅助函数会等待Promise完成,然后才返回最终的值。如果Promise给出否定结果,spawn中的yield则会抛出一个异常,我们可以用try/catch捕捉到。这样写异步代码真是超级简单!

PromiseAPI参考

除非额外注明,最新版的Chrome(Canary)和Firefox(nightly)均支持下列所有方法。这个Polyfill则在所有浏览器内实现同样的接口。

静态方法

Promise.cast(promise);
返回一个Promise(当且仅当promise.constructor==Promise)
备注:目前仅有Chrome实现

Promise.cast(obj);
创建一个以obj为成功结果的Promise。
备注:目前仅有Chrome实现

Promise.resolve(thenable);
从thenable对象创建一个新的Promise。一个thenable(类Promise)对象是一个带有“then”方法的对象。如果你传入一个原生的JavaScriptPromise对象,则会创建一个新的Promise。此方法涵盖了Promise.cast的特性,但是不如Promise.cast更简单高效。

Promise.resolve(obj);
创建一个以obj为确认结果的Promise。这种情况下等同于Promise.cast(obj)。

Promise.reject(obj);
创建一个以obj为否定结果的Promise。为了一致性和调试便利(如堆栈追踪),obj应该是一个Error实例对象。

Promise.all(array);
创建一个Promise,当且仅当传入数组中的所有Promise都确认之后才确认,如果遇到数组中的任何一个Promise以否定结束,则抛出否定结果。每个数组元素都会首先经过Promise.cast,所以数组可以包含类Promise对象或者其他对象。确认结果是一个数组,包含传入数组中每个Promise的确认结果(且保持顺序);否定结果是传入数组中第一个遇到的否定结果。
备注:目前仅有Chrome实现

Promise.race(array);
创建一个Promise,当数组中的任意对象确认时将其结果作为确认结束,或者当数组中任意对象否定时将其结果作为否定结束。
备注:我不大确定这个接口是否有用,我更倾向于一个Promise.all的对立方法,仅当所有数组元素全部给出否定的时候才抛出否定结果
献花(0)
+1
(本文系thedust79首藏)