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异步编程发展的脉络。