资讯专栏INFORMATION COLUMN

回调地狱-编写异步JavaScript指南

刘玉平 / 347人阅读

摘要:什么是回调地狱异步代码,或者说使用的代码,很难符合我们的直观理解。人们理解回调的最大障碍在于理解一个程序的执行顺序。怎样解决回调地狱问题糟糕的编码习惯造成了回调地狱。把回调函数的第一个参数设置为对象,是中处理异常最流行的方式。

什么是“回调地狱”?

异步Javascript代码,或者说使用callback的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代码时,误认为代码是按照我们看到的代码顺序从上到下执行的,这就是造成回调地狱的原因。在其他语言中,例如C,Ruby或者Python,第一行代码执行结束后,才会开始执行第二行代码,按照这种模式一直到执行到当前文件中最后一行代码。随着你学习深入,你会发现JavaScript跟他们是不一样的。

什么是回调(callback)?

某种使用JavaScript函数的惯例用法的名字叫做回调。JavaScript语言中没有一个叫“回调”的东西,它仅仅是一个惯例用法的名字。大多数函数会立刻返回执行结果,使用回调的函数通常会经过一段时间后才输出结果。名词“异步”,简称“async”,只是意味着“这将花费一点时间”或者说“在将来某个时间发生而不是现在”。通常回调只使用在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函数,当下载结束,回调会被调用。如果下载成功,传入photo给回调;下载失败,传入error给回调。

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函数完成了它的任务才会执行。这可能需要很长一段时间(取决于网速的快慢)。

这个例子意在阐明两个重要的概念:

handlePhoto回调只是一个存放将来进行的操作的方式

事情发生的顺序并不是直观上看到的从上到下,它会当某些事情完成后再跳回来执行。

怎样解决“回调地狱”问题?

糟糕的编码习惯造成了回调地狱。幸运的是,编写优雅的代码不是那么难!

你只需要遵循三大原则

1. 减少嵌套层数(Keep your code shallow)

下面是一堆乱糟糟的代码,使用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
  })
}

如你所见,给匿名函数一个名字是多么简单,而且好处立竿见影:

起一个一望便知其函数功能的名字让代码更易读

当抛出异常时,你可以在stacktrace里看到实际出异常的函数名字,而不是"anonymous"

允许你合理安排函数的位置,并通过函数名字调用它

现在我们可以把这些函数放在我们程序的顶层。

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.模块化(Modularize)

任何人都有有能力创建模块,这点非常重要。Isaac Schlueter(NodeJS项目成员)说过“写出一些小模块,每个模块只做一件事情,然后把他们组合起来放入其他的模块做一个复杂的事情。只要你不想陷入回调地狱,你就不会落入那般田地。”

让我们把上面的例子修改一下,改为一个模块。

下面是一个名为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模块化的用法。现在已经有了 formuploader.js 文件,我们只需要引入它并使用它。请看下面的代码:

var formUploader = require("formuploader")
document.querySelector("form").onsubmit = formUploader.submit

我们的应用只有两行代码并且还有以下好处:

方便新开发人员理解你的代码 -- 他们不需要费尽力气读完formuploader函数的全部代码

formuploader可以在其他地方复用

3.处理每一个异常(Handle every single error)

有三种不同类型的异常:语法异常,运行时异常和平台异常。语法异常通常由开发人员在第一次解释代码时捕获,运行时异常通常在代码运行过程中因为bug触发,平台异常通常由于没有文件的权限,硬盘错误,无网络链接等问题造成。这一部分主要来处理最后一种异常:平台异常。

前两个大原则意在提高代码可读性,但是第三个原则意在提高代码的稳定性。在你与回调打交道的时候,你通常要处理发送请求,等待返回或者放弃请求等任务。任何有经验的开发人员都会告诉你,你从来不知道哪里回出现问题。所以你有必要提前准备好,异常总是会发生。

把回调函数的第一个参数设置为error对象,是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
 }

把第一个参数设为error对象是一个约定俗成的惯例,提醒你记得去处理异常。如果它是第二个参数,你更容易把它忽略掉。

总结

不要嵌套使用函数。给每个函数命名并把他们放在你代码的顶层

利用函数提升。先使用后声明。

处理每一个异常

编写可以复用的函数,并把他们封装成一个模块

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/99949.html

相关文章

  • Node.js 入门你需要知道的 10 个问题

    摘要:什么是在中什么时候需要是中的包管理器。允许我们为安装各种模块,这个包管理器为我们提供了安装删除等其它命令来管理模块。 showImg(https://user-gold-cdn.xitu.io/2019/7/11/16bde5b2df52a924?w=4000&h=2667&f=jpeg&s=450648); 本文为您分享「Node.js 入门你需要知道的 10 个问题」这些问题可能也...

    szysky 评论0 收藏0
  • 【翻译】关于回调地狱

    摘要:回调地狱异步程序书写指南什么是回调地狱我们很难一眼就看懂异步,或者是使用回调函数的程序。通常回调函数会用在下载文件读取文件或者数据库相关事务等。注意还没有被调用,它只是被创建然后最为回调函数传入。 回调地狱 JavaScript异步程序书写指南 什么是回调地狱? 我们很难一眼就看懂异步JavaScript,或者是使用回调函数的JavaScript程序。例如下面这段代码: fs.read...

    Betta 评论0 收藏0
  • JavaScript引擎是如何工作的?从调用栈到Promise你需要知道的一切

    摘要:最受欢迎的引擎是,在和中使用,用于,以及所使用的。单线程的我们说是单线程的,因为有一个调用栈处理我们的函数。也就是说,如果有其他函数等待执行,函数是不能离开调用栈的。每个异步函数在被送入调用栈之前必须通过回调队列。 翻译:疯狂的技术宅原文:https://www.valentinog.com/bl... 本文首发微信公众号:前端先锋欢迎关注,每天都给你推送新鲜的前端技术文章 sh...

    Simon_Zhou 评论0 收藏0
  • 谈谈JavaScript异步代码优化

    摘要:异步问题回调地狱首先,我们来看下异步编程中最常见的一种问题,便是回调地狱。同时使用也是异步编程最基础和核心的一种解决思路。基于,目前也被广泛运用,其是异步编程的一种解决方案,比传统的回调函数解决方案更合理和强大。 关于 微信公众号:前端呼啦圈(Love-FED) 我的博客:劳卜的博客 知乎专栏:前端呼啦圈 前言 在实际编码中,我们经常会遇到Javascript代码异步执行的场景...

    chnmagnus 评论0 收藏0
  • 《Node.js设计模式》基于回调异步控制流

    摘要:编写异步代码可能是一种不同的体验,尤其是对异步控制流而言。回调函数的准则在编写异步代码时,要记住的第一个规则是在定义回调时不要滥用闭包。为回调创建命名函数,避免使用闭包,并将中间结果作为参数传递。 本系列文章为《Node.js Design Patterns Second Edition》的原文翻译和读书笔记,在GitHub连载更新,同步翻译版链接。 欢迎关注我的专栏,之后的博文将在专...

    Chiclaim 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<