• 【译】回调地狱 Callback Hell


    翻译自:http://callbackhell.com/,水平有限,做个人理解之用。

    这是一个编写异步JavaScript程序的指导手册。

    一、什么是回调地狱?

    异步的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))
            }
          })
        })
      }
    })
    有没有看到这些以"})"结尾的金字塔结构?这个形状为“亲切地”称为回调地狱。

    二、什么是callback(回调)?

    “callback"仅仅只是一种使用JavaScript函数的一种通用称呼。在JavaScript语言中,并没有一种特定的东西称之为“callback”,这个只是一种方便的称呼。不同于大部分立即返回结果的函数,这些使用callback的函数需要消耗一些时间才能返回结果。“asynchronous”(异步),或者简称为“async”仅仅表示需要花费一些时间,或者是“在未来发生,而不是现在”。通常情况下,callback仅仅用于操作I/O的时候使用到。比如下载、读写文件、与数据库交互等。
     当调用一个普通的函数的时候,你可以这样使用返回值:
    var result = multiplyTwoNumbers(5, 10)
    console.log(result)
    // 50 gets printed out
    然而,异步函数,也就是使用了callback函数的不会立刻返回任何东西。
    var photo = downloadPhoto('http://coolcats.com/cat.gif')
    // photo is 'undefined'!
    在这种情况下,下载gif文件会花费相当长的时间,而且你并不希望你的程序在等待下载结束的过程中处于“暂停”(也就是阻塞,block)状态。
    相反,你可以把下载结束后需要执行的操作存放到一个函数中,这个就是callback(回调)!你提供了一个“downloadPhoto”的函数,并且这个函数会在下载完成的时候执行callback(call you back later)函数,并且传递photo参数(或者出错的时候返回一个错误信息)。
    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')
    人们在尝试理解“callback”这个概念的最大困难之处在于,程序运行的过程中,程序中的代码,是怎么样按照规则执行的。在这个例子中,有三件主要的代码段会发生:首先 “handlePhoto”函数被申明,然后是“downloadPhoto”函数会被调用并且把“handlePhoto”函数作为回调函数“callback”参数传递进入,最后,打印一句话“Download started”。
    要注意以下,“handlePhoto”并没有立即被调用,只是创建并且作为一个参数传递给了“downloadPhoto”。但是,一旦“downloadPhoto”函数执行完成之后,”handlePhoto“就会运行。这个取决于网络连接到底有多快。
    这个例子试图说明两个重要的概念:
    (1)回调函数“handlePhoto”仅仅是一种“存放”操作的方式,而这些操作需要延迟一段时间之后进行。
    (2)代码的执行规则并不是按照阅读代码的“从上到下”的方式去遵守的,代码执行会根据事情结束的时间跳转嵌套。

    三、我们如何解决“回调地狱”?

    回调地狱的产生往往来源于对编码练习的缺乏,幸运的是,写出更好的代码并不困难!
    我们只需要遵循如下三个原则:

    (1)保持代码浅显易读

    下面是一个杂乱的JavaScript代码,这个代码用于通过使用“ 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
      })
    }
    正如你看到的,给函数进行命名是一件超级简单的事情,而且会立刻体验到几个好处:

    1)感谢这些具有描述性意义的函数名称把,这些名称使代码更加容易地阅读;

    2)当异常发生的时候,你会在异常堆栈中看到确切的函数名称,而不是“anonymous”之类的名字;

    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 hoisting.

    (2)模块化

    这个是最重要的部分:任何人都有能力创建模块。引用 Isaac Schlueter (来源于node.js项目)的话说:

    Write small modules that each do one thing, and assemble them into other modules that do a bigger thing. You can't get into callback hell if you don't go there.

    “编写一个个小的模块,每个模块完成一件事情,然后把他们组装起来,去完成一个更大的事情,回调地狱这个坑,你不去往那走,你是不会陷进去的”。
    让我们从上面的代码中提取模板代码,然后通过拆分到一组文件中的方式,将这些模板代码组装成module。我会展示一个module的格式,这种格式既可以用于浏览器的代码,也可以用在服务端。
    这个是一个新的文件,叫做“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模块系统的一个例子,electron 和 浏览器 使用browserify 。我非常喜欢这种模块化,因为它可以工作在任何地方,而且非常简单,并且不需要复杂的配置文件或者脚本。
    现在我们有了“formuploader.js”(并且已经作为一个脚本,在页面加载完成之后载入到了页面中),我们只需要“require”这个模块并且使用它!这个是一个我们的程序中具体代码的样子:
    var formUploader = require('formuploader')
    document.querySelector('form').onsubmit = formUploader.submit
    这样,我们的程序仅仅需要两行代码,并且有如下的好处:

    1)对于一个新的开发者来说,更加容易理解了 ------ 他们不用深陷于“被迫通读全部“formuploader”函数”。

    2)“formuploader”可以用于其他地方,不用重复编写代码,并且可以轻松地分享到github或者npm上去。

    (3)处理每一个独立的异常

    错误(error)有很多种类型:程序员犯的语法错误(往往在第一次尝试运行程序的时候会被发现);程序员犯的运行时错误(程序可以运行但是里面有一个bug会把事情弄糟);其他情况下的平台错误比如无效的文件权限,硬件驱动失效、网络连接异常等问题。这个部分主要针对最后一类错误(error)。
    前面两条原则可以让你的代码具有可读性,但是这一条可以让你的代码保持稳定健壮。当你在使用回调函数(callback)的时候,讲道理你其实是在和任务打交道,这些任务都是被分发给回调函数,并且回调函数会在后台执行,然后这个任务要么执行成功,要么由于失败而终止。任何有经验的开发者都会告诉你:你永远不会知道错误是谁么时候会发生,你只有去假定他们会一直出现。
    目前在回调函数中处理错误最流行的方式是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
    }
    在第一个参数中使用“error”是一个简单方便的鼓励记得处理错误的一个方式。如果把这个参数放到第二的位置,你在写代码的时候往往会容易忽略第二个“error”参数,而只关注第一个参数,比如“function handleFile(file){}”。
    Code linters(检查代码的小工具)也可以通过配置,实现提醒你要处理这些回调函数错误。最简单的一个小工具就是 standard。这个工具你仅仅只需要在你的代码文件的路径中执行 “$ standard”命令,它就会把你每一个没有进行错误处理的回调函数标记出来。

    四、总结

    1、不要嵌套函数。给这些函数进行命名,并且放到你的程序的最顶层。

    2、使用 function hoisting(函数提升)机制将你的函数移到文件的末尾。

    3、在每一个回调函数中去处理每一个错误。可以使用一个代码检查工具去帮你完成这个事情。

    4、创建可以服用的函数,并且把他们放置在一个模块中,这样可以提高代码可读性。把代码分割成一个个小的部分,可以帮助你更好的处理error,测试,强迫你去为你的代码创建一个稳定的、文档完善的公共API模块,而且有助于代码的重构。

    最重要的避免回调地狱的方面就是,移出你的函数,这样程序的流程可以更容易理解,新手也就不用去啃每一个函数究竟是干什么的。
    从现在开始,你首先就可以把函数移到文件的底部,然后逐渐地把函数移到另一个文件中并且使用类似“require('./photo-helpers.js')”的方式去关联,最终,把他们放进一个独立的模块比如“require('image-resize')”.

    下面是一些创建模块的一些原则:

    1、通过把一些经常重复使用的代码封装成一个函数;

    2、当你的函数(或者一组具有类似主题功能的函数)足够大的时候,移动到另一个文件,并且通过“module.exports“的方式去发布,你可以使用类似“require('./photo-helpers.js')”的方式去关联这个文件。

    3、如果你的代码可以用于很多个项目的时候,你需要提供“readme”文件、测试以及“package.json”文件,并且把他们发布到github和npm中。

    4、一个优秀的模块是很小的而且只聚焦于一个问题;

    5、JavaScript的模块中的一个独立的文件行数不应该超过150行;

    5、在整个JavaScript的文件结构组织中,一个模块不应该拥有超过一层的嵌套文件夹。如果这种情况发生了,那么意味着整个模块要做的事情有点过多了。

    6、让更有经验的程序员给你演示下好的模块构建的方式,直到你了解究竟什么是优秀的模块。如果有一个模块,你需要花不止几分钟的时间去了解它是干嘛的,那么这个模块并不是一个多么好的模块。

  • 相关阅读:
    断点下载
    根据显示的字符多少来做Label的自适应高度
    iOS中POST异步请求
    iOS中两个APP之间的跳转和通信
    cocoapod [!] /usr/bin/curl -f -L -o /var/folders/dj/yccslvys6tb53k2vz87djfsh0000gn/T/d20170219-12508-z77a4l/file.zip https://github.com/kylefleming/opencv/releases/download/3.1.0-ios-fix/opencv2.fram
    使用webview加载html图片、表单超屏幕问题
    uiwebview 加载html时字体变小 加载前或加载后改变字体大小
    uitabbarController tababr 上方横线隐藏
    uinavigationcontroller uinavigationbar 下方横线去除
    贝赛尔曲线 绘制园
  • 原文地址:https://www.cnblogs.com/shenggang/p/6297587.html
Copyright © 2020-2023  润新知