[译]回调地狱
——译自《Callback Hell》
这是一篇有关编写异步javascript程序的指南
什么是“回调地狱”?
异步js代码,或者用到了回调函数的js代码,都很难直观理解。这类代码看起来大多数像这样:
fs.readdir(source, function(err, files) { if (err) { console.log('Error finding files: ' + err) } else { files.forEach(function(filename, fileIndex) { console.log(filename) gm(source + filename).size(function(err, values) { if (err) { console.log('Error identifying file size: ' + err) } else { console.log(filename + ' : ' + values) aspect = (values.width / values.height) widths.forEach(function(width, widthIndex) { height = Math.round(width / aspect) console.log('resizing ' + filename + 'to ' + height + 'x' + height) this.resize(width, height).write(destination + 'w' + width + '_' + filename, function(err) { if (err) console.log('Error writing file: ' + err) }) }.bind(this)) } }) }) } })
艾玛,到处都是function
和})
!是不是很眼熟?这就是我们“亲(坑)爱(爹)的”回调地狱。
其实,只需要知道以下一些小技巧,妈妈就再也不用担心你写不出直观易懂的代码:
给你的函数命名
下面是一段略显杂乱的浏览器js代码,这段代码利用browser-request来向服务端发送AJAX请求:
var form = document.querySelector('form'); form.onsubmit = function(submitEvent) { var name = document.querySelector('input').value request({ uri: "http://example.com/upload", body: name, method: "POST" }, function(err, response, body) { var statusMessage = document.querySelector('.status') if (err) return statusMessage.value = err statusMessage.value = body }) }
这段代码包含了两个匿名函数,现在我们给它们命名。
var form = document.querySelector('form') form.onsubmit = function formSubmit(submitEvent) { var name = document.querySelector('input').value request({ uri: "http://example.com/upload", body: name, method: "POST" }, function postResponse(err, response, body) { var statusMessage = document.querySelector('.status') if (err) return statusMessage.value = err statusMessage.value = body }) }
你看,给函数命名其实非常简单,而且这样做还能让你的代码达到意想不到的效果。
- 代码更易读
- 代码抛异常的时候,你能很快定位到抛错的函数名,而不是定位到“匿名”函数
- 使代码层级变浅,至少不会出现很深的嵌套关系,这也是我下面要讲的:
保持浅的代码风格
在上述例子的基础上,我们进一步改掉上面代码里的三级嵌套:
function formSubmit(submitEvent) { var name = document.querySelector('input').value request({ uri: "http://example.com/upload", body: name, method: "POST" }, postResponse) } function postResponse(err, response, body) { var statusMessage = document.querySelector('.status') if (err) return statusMessage.value = err statusMessage.value = body } document.querySelector('form').onsubmit = formSubmit
这样的代码就好看多了,而且以后不管是编写,重构或者是拆分都会更加容易哦。
模块化
现在是最重要的环节: 任何人都能创建模块(也就是代码库)。引用Isaac Schlueter(node.js项目的成员)的一句话:“一个小模块只需要实现一个单一功能,然后把小模块们组装到其它要实现更大功能的模块中。只要你稍微注意就不会陷入回调地狱。”
现在我们把上面的示例代码提取出来,通过将其拆成几个文件来创建一个模块。既然得同时编写浏览器端和服务端的JavaScript,我下面展示的这种方式能让代码在浏览器和服务端都可以运行,而且代码还超简洁。
下面这个formuploaded.js
是一个新文件,包含了我们之前写的那两个函数:
function formSubmit(submitEvent) { var name = document.querySelector('input').value request({ uri: "http://example.com/upload", body: name, method: "POST" }, postResponse) } function postResponse(err, response, body) { var statusMessage = document.querySelector('.status') if (err) return statusMessage.value = err statusMessage.value = body } exports.submit = formSubmit
代码最后的 exports
是 CommonJS 模块系统的一个例子,编写服务端 javascript程序的Node.js就用到了这个。我非常喜欢这种模块化的方式,因为它真的很简单 —— 你只需要定义一些接口,当其它地方加载这个模块时就能够调用了(这就是exports
的好处)。
要在浏览器里使用这些CommonJS模块时,你可以借助一个叫browserify的命令行工具。我在这儿就不详细教大家怎么用了,总之借助这个工具你就能用require
方式加载模块代码到你的程序里。
现在我们有了formuploader.js
文件(并且它已经通过script标签加载到页面里了),我们只需要require这个文件就能使用它提供的方法了,我们的应用程序代码就变成了下面这个样子:
function formSubmit(submitEvent) { var name = document.querySelector('input').value request({ uri: "http://example.com/upload", body: name, method: "POST" }, postResponse) } function postResponse(err, response, body) { var statusMessage = document.querySelector('.status') if (err) return statusMessage.value = err statusMessage.value = body } exports.submit = formSubmit
现在我们的应用程序只有两行代码了,并且多了以下好处:
- 新手也能轻松看懂代码 —— 他们不会因为阅读整个
formuploader
的函数而卡壳。 formuploader
不必拷贝代码就可以用在任何地方,而且可以在github上轻松地分享给大家。- 代码本身也很优美,简单易读。 在web浏览器和服务端有很多模块模式,有些很复杂。我在本文中展示的模块方式是我认为最最容易理解的。
我还是不太理解本文
可以去读一下我的回调简介
什么是promises?
Promises是一个处理Javascript中异步编程的更抽象的模式。
这篇文章旨在展示如何编写优美的javascript代码。如果你使用了第三方库来抽象化你的JS代码,那你得保证你愿意推动所有参与你的项目的人都跟你保持一样的对JS的看法。
我的个人经历是,在90%的异步代码中我都用了回调方式,当代码变得复杂时我就尝试引入一些第三方库,比如async来收拾烂摊子。
话虽如此,每个开发者都有其独特的开发风格,你也应尽量保持你习惯的风格。你只需记住事无绝对:有些人就只喜欢用回调方式,有些人却不是。
补充阅读
有兴趣的话你可以在github上fork这个项目!欢迎贡献新内容或者修正已有内容。