javascript学习指南之回调问题
回调地狱
对JavaScript程序员来说,处理回调是家常,但是处理层次过深的回调就没有那么美好了,下面的示例代码片段用了三层回调,再补脑一下更多层的场景,简直是酸爽,这就是传说中的回调地狱。
getDirectories(function(dirs){ getFiles(dirs[0],function(files){ getContent(files[0],function(file,content){ console.log('filename:',file); console.log(content); }); }); }); functiongetDirectories(callback){ setTimeout(function(){ callback(['/home/ben']); },1000); } functiongetFiles(dir,callback){ setTimeout(function(){ callback([dir+'/test1.txt',dir+'/test2.txt']); },1000) } functiongetContent(file,callback){ setTimeout(function(){ callback(file,'content'); },1000) }
解决方案
生态圈中有很多异步解决方案可以处理回调地狱的问题,比如bluebird、Q等,本文重点介绍ECMAScript6/7规范中对异步编程的支持。
ES6Promise
Promise是一种异步编程的解决方案,是解决回调地狱问题的利器。
Promise在JavaScript生态圈被主流接受是在2007年Dojo框架增加了dojo.Deferred的功能。随着dojo.Deferred的流行,在2009年KrisZyp提出了CommonJSPromises/A规范。随后生态圈中出现了大量Promise实现包括Q.js、FuturesJS等。当然Promise之所有这么流行很大程度上是由于jQuery的存在,只是jQuery并不完全遵守CommonJSPromises/A规范。随后正如大家看到的,ES6规范包含了Promise。
MDN中对Promise是这样描述的:
Promise对象是一个返回值的代理,这个返回值在promise对象创建时未必已知。它允许你为异步操作的成功或失败指定处理方法。这使得异步方法可以像同步方法那样返回值:异步方法会返回一个包含了原返回值的
以下的代码是「回调地狱」一节中的示例通过Promise实现,看上去代码也不是很简洁,但是比起传统的层级回调有明显改善,代码可维护性和可读性更强。
getDirectories().then(function(dirs){ returngetFiles(dirs[0]); }).then(function(files){ returngetContent(files[0]); }).then(function(val){ console.log('filename:',val.file); console.log(val.content); }); functiongetDirectories(){ returnnewPromise(function(resolve,reject){ setTimeout(function(){ resolve(['/home/ben']); },1000); }); } functiongetFiles(dir){ returnnewPromise(function(resolve,reject){ setTimeout(function(){ resolve([dir+'/test1.txt',dir+'/test2.txt']); },1000); }); } functiongetContent(file){ returnnewPromise(function(resolve,reject){ setTimeout(function(){ resolve({file:file,content:'content'}); },1000); }); }
ES6Generator
Promise的实现方式还不够简洁,我们还需要更好的选择,co就是选择之一。co是基于Generator(生成器)的异步流控制器,了解co之前首先需要理解Generator。熟悉C#的同学应该都有了解,C#2.0的版本就引入了yield关键字,用于迭代生成器。ES6Generator跟C#相似,也使用了yield语法糖,内部实现了状态机。具体用法可以参考MDN的文档function*一节,原理可以参考AlloyTeam团队Blog深入理解Generator。使用co巧妙结合ES6Generator和ES6Promise让异步调用更加和谐。
co(function*(){ vardirs=yieldgetDirectories(); varfiles=yieldgetFiles(dirs[0]); varcontentVal=yieldgetContent(files[0]); console.log('filename:',contentVal.file); console.log(contentVal.content); });
co非常巧妙,其核心代码可以简化如下的示例,大体思路是采用递归遍历生成器直到状态完成,当然co做的跟多。
runGenerator(); function*run(){ vardirs=yieldgetDirectories(); varfiles=yieldgetFiles(dirs[0]); varcontentVal=yieldgetContent(files[0]); console.log('filename:',contentVal.file); console.log(contentVal.content); } functionrunGenerator(){ vargen=run(); functiongo(result){ if(result.done)return; result.value.then(function(r){ go(gen.next(r)); }); } go(gen.next()); }
ES7Async/Await
ES6Generator确实很好,只可惜需要第三方库的支持。好消息是ES7会引入Async/Await关键字完美解决异步调用的问题。好吧,.net又领先了一步,.netframework4.5已经率先支持了。
今后的代码写起来是这样:
run(); asyncfunctionrun(){ vardirs=awaitgetDirectories(); varfiles=awaitgetFiles(dirs[0]); varcontentVal=awaitgetContent(files[0]); console.log('filename:',contentVal.file); console.log(contentVal.content); }
结论
从经典的回调的异步编程方式,到ES6Promise规范对异步编程的改善,再到co结合ESGenerator优雅处理,最后ES7async/await完美收官,可以让我们了解为什么ECMAScript会出现这些特性以及解决了什么问题,更加清晰地看到JavaScript异步编程发展的脉络。