回调地狱

本文首发于微信公众号平台(itclancoder),若是你想阅读体验更好,能够戳连接回调地狱node

从前一文中你真的了解回调咱们已知道回调函数是必须得依赖另外一个函数执行调用,它是异步执行的,也就是须要时间等待,典型的例子就是Ajax应用,好比http请求,在不刷新浏览器的状况下,当你执行DOM事件时,好比页面上点击某连接,回车等事件操做,浏览器会悄悄向服务端发送若干http请求,携带后台可识别的参数,等待服务器响应返回数据,这个过程是异步回调的,当许多功能须要连续调用,环环相扣依赖时,它就相似下面的代码,代码所有一层一层的嵌套,看起来就很庞大,很恶心,就产生了回调地狱.本文,将为你揭晓怎么避免回调地狱,您将在本文中了解到如下内容:react

  • 什么是回调地狱(函数做为参数层层嵌套)git

  • 什么是回调函数(一个函数做为参数须要依赖另外一个函数执行调用)程序员

  • 如何解决回调地狱github

    • 保持你的代码简短(给函数取有意义的名字,见名知意,而非匿名函数,写成一大坨)数据库

    • 模块化(函数封装,打包,每一个功能独立,能够单独的定义一个js文件Vue,react中经过import导入就是一种体现)npm

    • 处理每个错误编程

    • 建立模块时的一些经验法则json

    • 承诺/生成器/ES6等promise

  • Promises:编写异步代码的一种方式,它仍然以自顶向下的方式执行,而且因为鼓励使用try / catch样式错误处理而处理更多类型的错误

  • Generators:生成器让你“暂停”单个函数,而不会暂停整个程序的状态,但代码要稍微复杂一些,以使代码看起来像自上而下地执行

  • Async functions:异步函数是一个建议的ES7功能,它将以更高级别的语法进一步包装生成器和继承

什么是“回调地狱”?

异步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))
        }
      })
    })
  }
})
复制代码

看到最后的金字塔形状和全部})?这被亲切地称为回调地狱

回调地狱的缘由是,当人们试图以一种从上到下的视觉方式执行JavaScript的方式编写JavaScript时。不少人犯这个错误!在C,Ruby或Python等其余语言中,指望第1行发生的任何事情都会在第2行的代码开始运行以前完成,依此类推。正如你将会学到的,JavaScript是不一样的

什么是回调函数?

回调只是使用JavaScript函数的惯例的名称。 JavaScript语言中没有特别的东西叫作“回调”,它只是一个约定。不像大多数函数那样当即返回一些结果,使用回调函数须要一些时间来产生结果。 意思是“须要一些时间”或“未来会发生,而不是如今”。一般回调仅在进行I / O时使用,例以下载东西,阅读文件,与数据库交互等

当你调用一个普通的函数时,你可使用它的返回值

var result = multiplyTwoNumbers(5, 10)
console.log(result // 50 gets printed out
复制代码

然而,异步和使用回调的函数不会当即返回任何内容

var photo = downloadPhoto('http://coolcats.com/cat.gif')
   // photo is 'undefined'!
复制代码

在这种状况下,gif可能须要很长时间才能下载,而且你不但愿程序在等待下载完成时暂停()

相反,你存储在功能下载完成后应运行的代码。这是回调!你把它给到downloadPhoto功能,它会在下载完成时运行你的回调(例如'之后再打电话给你'),而且传递照片(或者若是出现错误,会出错)

downloadPhoto('http://coolcats.com/cat.gif', handlePhoto)

function handlePhoto (error, photo) {
  if (error) console.error('Download error!', error)
  else console.log('Download finished', photo)
}

console.log('Download started')
复制代码

人们在尝试理解回调时遇到的最大障碍是理解程序运行时执行的顺序。在这个例子中发生了三件事情。首先声明handlePhoto函数,而后调用downloadPhoto函数并传递handlePhoto做为其回调函数,最后打印出“Download started”

请注意,handlePhoto还没有被调用,它只是被建立并做为回调传入downloadPhoto。但直到downloadPhoto完成其任务后才能运行,这可能须要很长时间,具体取决于Internet链接的速度

这个例子是为了说明两个重要的概念

  • handlePhoto回调只是稍后存储一些事情的一种方式
  • 事情发生的顺序不是从顶部到底部读取,而是基于事情完成时跳转

我该如何解决回调地狱?

回调地狱是因为糟糕的编码习惯形成的。幸运的是,编写更好的代码并不困难! 你只需遵循三条规则:

1. 保持你的代码简短

这里有一些凌乱的浏览器JavaScript,它使用浏览器请求向服务器发送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
  })
}
复制代码

这段代码有两个匿名函数。让咱们给他们的名字formSubmit与postResponse

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
  })
}
复制代码

正如你所看到的,命名函数很是简单而且有一些直接的好处

  • 因为描述性功能名称,使代码更容易阅读
  • 当发生异常时,你将得到引用实际函数名称而不是“匿名”的堆栈跟踪
  • 容许你移动功能并按名称引用它们

如今咱们能够将这些功能移到咱们程序的顶层

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
    }
复制代码

请注意,这里的函数声明是在文件底部定义的。这要归功于提高功能

2. 模块化

这是最重要的部分:任何人都有能力建立模块。引用(node.js项目的)Isaac Schlueter的话:“编写一个小模块,每一个模块都作一件事,而后将它们组装成其余模块,作更大的事情。若是你不去那里,你不能进入回调地狱

让咱们从上面取出样板代码,并将其分红几个文件,将其转换为模块。我将展现一个适用于浏览器代码或服务器代码的模块模式(或者适用于二者的代码)

这是一个名为formuploader.js的新文件,它包含咱们以前的两个函数

module.exports.submit = 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
}
复制代码

module.exports位是node.js模块系统的一个例子,它在node,Electron和使用browserify的浏览器中工做。我很是喜欢这种模式,由于它能够在任何地方工做,理解起来很是简单,而且不须要复杂的配置文件或脚本

如今咱们已经有了formuploader.js(而且在浏览器中将它做为脚本标签加载到页面中),咱们只须要它并使用它!如下是咱们如今的应用程序特定代码的外观

var formUploader = require('formuploader')
document.querySelector('form').onsubmit = formUploader.submit
复制代码

如今咱们的应用程序只有两行代码,并具备如下优势:

  • 新开发人员更容易理解 - 他们不会因阅读全部formuploader函数而陷入困境
  • ormuploader能够在其余地方使用,无需复制代码,而且能够轻松地在github或npm上共享

3. 处理每个错误

有不一样类型的错误:由程序员形成的语法错误(一般在你尝试首次运行程序时发生),程序员形成的运行时错误(代码已运行但存在致使某些事情混乱的错误),平台错误由无用的文件权限,硬盘驱动器故障,无网络链接等引发的。这部分只是为了解决最后一类错误

前两条规则主要是关于让你的代码可读,但这是关于让代码稳定的。在处理回调时,你根据定义处理已分派的任务,请在后台执行某些操做,而后成功完成或因为失败而停止。任何有经验的开发人员都会告诉你,你永远没法知道这些错误什么时候发生,因此你必须对它们进行计划

经过回调,处理错误的最多见方法是Node.js方式,其中回调的第一个参数始终保留用于错误

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
 }
复制代码

有第一个参数是错误是一个简单的惯例,鼓励你记住处理你的错误。若是它是第二个参数,你能够编写像函数handleFile(file){}的代码,而且更容易忽略错误

代码库也能够配置为帮助你记住处理回调错误。最简单的使用称为标准。你所要作的就是在你的代码文件夹中运行$ standard,它会向你显示你的代码中的每个回调,并带有未处理的错误

小结

  1. 不要嵌套功能。给他们姓名并将他们放在程序的顶层
  2. 利用函数提高来利用你的优点来移动函数
  3. 处理每一个回调中的每个错误。使用标准来帮助你
  4. 建立可重用的函数并将它们放在模块中以减小理解代码所需的认知负载。将代码分割成小块这样也能够帮助您处理错误,编写测试,强制您为您的代码建立稳定且文档化的公共API,并有助于重构

避免回调地狱的最重要的方面是将功能移开,以便程序流程能够更容易理解,而无需新手参与功能的全部细节以了解程序正在尝试作什么

你能够先将函数移动到文件底部,而后使用require('./ photo-helpers.js')等相关需求将它们移动到另外一个文件中,而后将它们移动到独立模块像 require('image-resize'))

如下是建立模块时的一些经验法则:

  • 首先将重复使用的代码移入一个函数
  • 当你的函数(或与同一主题相关的一组函数)变得足够大时,将它们移动到另外一个文件中并使用module.exports将其公开。你可使用相对需求来加载它
  • 若是你有一些能够在多个项目中使用的代码,给它本身的readme,tests和package.json,并将它发布到github和npm。这里列出的具体方法有太多使人敬畏的好处
  • 一个好的模块很小,专一于一个问题
  • 模块中的单个文件不该超过150行左右的JavaScript
  • 一个模块不该该有多于一个嵌套文件夹级别的文件夹。若是是这样,它可能作了太多事情
  • 请你认识的更有经验的编程人员向你展现优秀模块的例子,直到你对他们的样子有了一个好的想法。若是须要花费几分钟时间才能了解正在发生的事情,那么它可能不是一个很好的模块

承诺/生成器/ES6等呢

在研究更先进的解决方案以前,请记住,回调是JavaScript的基本组成部分(由于它们只是函数),你应该在学习更先进的语言特性以前学习如何读写它们,由于它们都依赖于对回调。若是你还不能编写可维护的回调代码,请继续使用它

若是你真的但愿你的异步代码从头至尾阅读,你能够尝试一些奇特的东西。请注意,这些可能会引入性能和/或跨平台运行时兼容性问题

  • Promises:是编写异步代码的一种方式,它仍然以自顶向下的方式执行,而且因为鼓励使用try / catch样式错误处理而处理更多类型的错误
  • Generators生成器让你“暂停”单个函数,而不会暂停整个程序的状态,但代码要稍微复杂一些,以使代码看起来像自上而下地执行。
  • Async functions异步函数是一个建议的ES7功能,它将以更高级别的语法进一步包装生成器和承诺。

总结

回调地狱最主要的就是由于功能逻辑代码嵌套的层次太多,致使可读性下降,维护困难,避免回调地狱的最重要的方面是将功能移开,保持代码简单,不嵌套并分红小模块,也就是多多进行代码封装,将你所要的属性和方法用function关键字包裹起来,并且还要给它取一个有意义的名字,例如:页面上弹框,显示,隐藏,下拉等各个功能小模块,分别用有名函数给包裹起来,少用匿名函数,以即可以重复的屡次使用,这也是能够便于程序流程的理解

除了常见的一种回调函数做为异步处理,还有promises,Generators,async是处理异步处理的方式,,关于这三个我也在学习当中,理论的东西虽是概念,没有大量代码的编写,我的以为是很难理解这些东西,可是代码就是这些语言文字实实在在的转化,骚年们,加油,加油....

原文阅读出处

相关文章
相关标签/搜索