前端旧约

今天做别人不愿意做的事, 明天做别人不能做的事

回调地狱

原文链接:https://www.jianshu.com/p/39adf6ab8ad1

前言

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

  • 什么是回调地狱(函数作为参数层层嵌套)
  • 什么是回调函数(一个函数作为参数需要依赖另一个函数执行调用)
  • 如何解决回调地狱
    • 保持你的代码简短(给函数取有意义的名字,见名知意,而非匿名函数,写成一大坨)
    • 模块化(函数封装,打包,每个功能独立,可以单独的定义一个js文件Vue,react中通过import导入就是一种体现)
    • 处理每一个错误
    • 创建模块时的一些经验法则
    • 承诺/生成器/ES6等
  • 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
  })
}

这段代码有两个匿名函数。让我们给他们的名字

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')等相关需求将它们移动到另一个文件中,然后将它们移动到独立模块like 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是处理异步处理的方式,,关于这三个我也在学习当中,理论的东西虽是概念,没有大量代码的编写,个人觉得是很难理解这些东西,但是代码就是这些语言文字实实在在的转化,骚年们,加油,加油....

posted on 2020-03-31 20:04  前端旧约  阅读(331)  评论(0编辑  收藏  举报

导航