谈一下异步编程模型的一些优化(翻译)
tips
翻译的文章来自于 http://callbackhell.com/ 翻译过程中有掺杂个人的理解和翻译语法问题,如果英语质量过关,还请阅读原文。
废话不多说,先来看一下回调地狱
Asynchronous JavaScript, or JavaScript that uses callbacks, is hard to get right intuitively. A lot of code ends up looking like this
异步的JavaScript或者JavaScript使用了回调函数,是很难按照正确的顺序来直观的阅读的,有很多代码看起来像是下面这样子
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(dest + 'w' + width + '_' + filename, function(err) {
//第五层回调
if (err) console.log('Error writing file: ' + err)
})
}.bind(this))
}
})
})
}
})
如何避免回调地狱?
保持你的代码更加简单
下面使用ajax请求来做为举例说明:
button.onclick = function (submitEvent) {
var name = document.querySelector('input').value
request({
uri: "http://*.com/upload",
body: name,
method: "POST"
}, function (err, response, body) {
//成功之后的执行回调函数
var statusMessage = document.querySelector('.status')
if (err) return statusMessage.value = err
statusMessage.value = body
})
}
其实你可以发现,这里面有两个匿名函数,给匿名函数加上名字会清晰很多
// 在这里为方法添加一个命名,submitForm 提交方法
button.onclick = function submitForm(submitEvent) {
var name = document.querySelector('input').value
request({
uri: "http://*.com/upload",
body: name,
method: "POST"
},
// http响应方法处理
function httpResponse(err, response, body) {
//成功之后的执行回调函数
var statusMessage = document.querySelector('.status')
if (err) return statusMessage.value = err
statusMessage.value = body
})
}
添加了方法明明之后,你可以比较直观的去使用这个方法,有几点好处:
- 函数命名之后,代码更加容易阅读
- 当一个异常发生的时候,你可以通过栈读出来,而不是一个匿名函数(anonymous function)
- 允许你在其他地方重命名你的函数,并且可以引用
现在我们可以移动你的已经命名的方法:
document.querySelector('form').onsubmit = formSubmit
// 表单提交方法
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
}
注意: function 声明虽然是定义在文件的底部,调用确实在上面,这都要感谢JavaScript的方法提升的特性[https://gist.github.com/maxogden/4bed247d9852de93c94c]。
模块化
模块化最重要的就是:任何人都可以创建模块(aka libraries),引用node.js项目组的一句话就是:
编写每个模块做一件事情,然后将它们组装成其他模块做一件更大的事情。如果你不去那里,你就不会进入回调地狱。
我们再来看一下之前的回调方法:现在封装到一个formUploader.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
}
然后我们就可以调用方法类似于这样子:
var formUploader = require('formuploader')
document.querySelect('form').onsubmit = formUploader.submit;
现在你的代码就只剩下了两行
- 更好的开发 代码理解能力 ,开发者不需要全部理解formuploader.submit方法,就可以了
- 代码可以更好地被复用在npm或者github上
认真的处理回调过程中的每一个错误
错误有很多种,包括语法错误,运行时错误,平台错误之类,我们应当灵活的处理所有的bug。
前面的两个规则(保证代码简介、模块化)是为了保证代码更加直观,而这个规则是保证你的代码更加具有稳定性。
伴随着回调函数,node.js绝大多数的处理方式就是error优先返回,
var fs = require('fs')
fs.readFile('/Does/not/exist', handleFile)
function handleFile (error, file) {
if (error) return console.error('Uhoh, there was an error', error)
// otherwise, continue on and use `file` in your code
}
错误优先返回,是一种能让你记住处理错误的简单的方式,如果有第二个参数,你可以写一个function来处理,也可以更加简单的处理错误。
总结
- 不要随便使用匿名函数,给他们一个名字会好很多(最好是封装在你的程序的顶部)
- 使用方法提升来让你的方法更加优先定义。
- 在你的每一个回调方法中都单独处理每一个错误,使用标准化来规范自己的代码风格
- 创建一个可复用的方法,并且放入到module(模块)里面,让你的代码更加可读。分割你的代码到每一个小的方法中,可以更好分片的处理你的错误(error)。强迫你必须创建一个稳定且开放的代码API来约束你的代码,这对于以后的重构会有很大的帮助。
其实整体来讲,回调地狱真正恐怖的地方在于逻辑无法清晰的在代码层面读取出来,我们通过为方法命名,抽取出来,并且模块化可以更好地理解并且使用你的回调方法。
你也可以把方法(你想要重构的)抽取出来,放在文件的最下面(不会碍着你的正常看代码的风格),然后逐步的把文件都移动到其他文件中去,你也可以直接新建一个require(./helpper.js),例如这样子,然后再把你的方法逐步的重构出去。
创建模块的事后有一个规则要注意
- 首先是封装你最常见的重复代码逻辑
- 当你的方法(或者一组与方法相关的功能)变得足够多(多到你觉得可以封装功能的时候)移动他们到另外一个文件并且导出模块(module.exports),你可以使用相对路径加载这模块。
- 如果你有相同的代码在多个文件中,给他们相对应的readme.md然后测试用例以及package.json,最后发布到npm上面,在npm上已经有巨多无比的模块给你去用。
- 一个好的模块是足够小巧,且能够专注于解决问题的。
- 一个模块中,每一个文件都不应当超出150行左右的代码,否则要认真思考一下自己的业务逻辑是否有这么多复杂的代码。
- 一个模块不应当有更多两层以上的存放JavaScript文件的文件夹,否则你就要思考一下,你究竟是在解决什么问题。
- 想一个更加有经验的开发者来请教学习如何更好的封装一个模块,一直到在他们看起来你比他们有更好的想法。如果一个模块需要你话费好几分钟才能够理解他到底是能够做什么,那么他可能不是一个好的模块。