谈一下异步编程模型的一些优化(翻译)

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
  })
}

添加了方法明明之后,你可以比较直观的去使用这个方法,有几点好处:

  1. 函数命名之后,代码更加容易阅读
  2. 当一个异常发生的时候,你可以通过栈读出来,而不是一个匿名函数(anonymous function)
  3. 允许你在其他地方重命名你的函数,并且可以引用

现在我们可以移动你的已经命名的方法:

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来处理,也可以更加简单的处理错误。

总结

  1. 不要随便使用匿名函数,给他们一个名字会好很多(最好是封装在你的程序的顶部)
  2. 使用方法提升来让你的方法更加优先定义。
  3. 在你的每一个回调方法中都单独处理每一个错误,使用标准化来规范自己的代码风格
  4. 创建一个可复用的方法,并且放入到module(模块)里面,让你的代码更加可读。分割你的代码到每一个小的方法中,可以更好分片的处理你的错误(error)。强迫你必须创建一个稳定且开放的代码API来约束你的代码,这对于以后的重构会有很大的帮助。

其实整体来讲,回调地狱真正恐怖的地方在于逻辑无法清晰的在代码层面读取出来,我们通过为方法命名,抽取出来,并且模块化可以更好地理解并且使用你的回调方法。

你也可以把方法(你想要重构的)抽取出来,放在文件的最下面(不会碍着你的正常看代码的风格),然后逐步的把文件都移动到其他文件中去,你也可以直接新建一个require(./helpper.js),例如这样子,然后再把你的方法逐步的重构出去。

创建模块的事后有一个规则要注意

  • 首先是封装你最常见的重复代码逻辑
  • 当你的方法(或者一组与方法相关的功能)变得足够多(多到你觉得可以封装功能的时候)移动他们到另外一个文件并且导出模块(module.exports),你可以使用相对路径加载这模块。
  • 如果你有相同的代码在多个文件中,给他们相对应的readme.md然后测试用例以及package.json,最后发布到npm上面,在npm上已经有巨多无比的模块给你去用。
  • 一个好的模块是足够小巧,且能够专注于解决问题的。
  • 一个模块中,每一个文件都不应当超出150行左右的代码,否则要认真思考一下自己的业务逻辑是否有这么多复杂的代码。
  • 一个模块不应当有更多两层以上的存放JavaScript文件的文件夹,否则你就要思考一下,你究竟是在解决什么问题。
  • 想一个更加有经验的开发者来请教学习如何更好的封装一个模块,一直到在他们看起来你比他们有更好的想法。如果一个模块需要你话费好几分钟才能够理解他到底是能够做什么,那么他可能不是一个好的模块。

你可能感兴趣的:(谈一下异步编程模型的一些优化(翻译))