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的对立方法,仅当所有数组元素全部给出否定的时候才抛出否定结果 |
|