资讯专栏INFORMATION COLUMN

【翻译】关于回调地狱

Betta / 802人阅读

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

回调地狱

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

这个一堆以})结尾的金字塔,我们很亲切地称它为——“回调地狱”。

之所以会出现回调地狱,是因为我们写JavaScript一般是视觉上的从上到下书写。很多人犯了这个错误!在例如C、Ruby或者Python等其他语言,在第二行代码运行之前,第一行代码肯定已经运行完了。然而如后面所说的,JavaScript是不同的。

什么是回调函数?

回调函数是JavaScript里约定俗成的一个名称。实际上并不存在确定的“回调函数”,只是大家就管那个位置的函数作回调函数。与大多数运行后立刻给出结果的函数不同,使用回调的函数要花一些时间才能得出结果。“异步”这个词就是代表‘要花时间,将来运行’。通常回调函数会用在下载文件、读取文件、或者数据库相关事务等。

当你调用一个普通函数,你可以立刻得到它的值:

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函数调用,最后控制台打印出"Download started"

注意handlePhoto还没有被调用,它只是被创建然后最为回调函数传入downloadPhoto。直到downloadPhoto完成下载,他都不会运行。

这个例子说明两个问题:

handlePhoto(回调函数)只是储存了将要运行的东西

不要从上到下阅读程序,程序会根据事情完成而跳转

怎么修复回调地狱?

你只需要跟着一下三步走:

1.减少代码嵌套

以下是一些用于AJAX的浏览器端代码(使用browser-request):

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

你们看,给函数命名很简单,但是好处可不少:

有了函数名,可以很容易知道这段代码的作用

在控制台调试出错的时候,控制台会告诉你是哪个函数出错了,而不是一个匿名函数(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.模块化

用上面的例子,我们将把它拆分成多个文件,我会告诉你怎么把他做成模块。

创建一个包含前面两个函数的新文件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函数而陷入困境。

formuploader不用复制粘贴代码,只要在github或者npm下载分享的代码就可以了。

3.处理每一个错误

常见错误有几种

语法错误(运行失败)

运行时错误(可以运行但是有bug)

平台错误(文件权限问题、磁盘问题、网络问题)

前两条规则主要是提高你的代码的可读性,而这条是让你的代码更稳定。在处理回调时,您将根据定义处理发送的任务,在后台执行某些操作,最后成功完成或失败中止。任何有经验的开发人员都会告诉你,你永远不会知道这些错误发生什么时候发生,所以在问题出现时都必须有所对策。

最常用的回调错误处理是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是一个简单的共识,这样做可以提醒你必须处理你的错误。如果是第二个参数的话你很容易把代码写成function handleFile (file) { }然后就忘了处理错误。
代码规范化工具也可以提醒你添加回调错误处理,最简单的方法之一是使用standard。只是在你的文件目录运行 $ standard就能检查你的代码有没有缺少错误处理。

总结

不要嵌套函数,命名后调用更好

使用函数提升

处理回调函数的每一个错误

创建可重用函数,写成模块,让你更容易读懂代码。把你的代码拆分成小块可以帮助你处理错误,写测试,重构,方便为你的代码写更稳定的API

避免回调地狱的最重要的是移动函数,以便程序流程可以更容易地被理解,其他程序员可以不翻遍整个文件就能知道这段程序的功能。

你可以先把函数移动到底部,然后逐渐把函数写到模块文件里,然后使用require引入它(就像引用其他npm模块一样)。

一些写模块的经验:

先把经常重复使用的功能写成一个函数

当这个函数写得够大之后,把他移动到另一个文件,用module.exports暴露它,然后用require引入

如果你的代码是通用的,可以写readme文件和package.json发布到npm或者github

一个好模块,体积要小,而且针对只一个问题

模块中的单个文件不应超过约150行

模块不应该有多个级别的嵌套文件夹,其中包含JavaScript文件。如果是这样,它可能做的太多了

让有经验的程序员介绍你一些好用的模块,尝试理解这个模块的功能,如果花了几分钟的话,这个模块可能就不够好了

关于promise/生成器/ES6?

在查看更高级的解决方案之前,请记住,回调是JavaScript的一个基本部分(因为它们只是函数),你应该学习如何读写它们,然后再转向更高级的语言功能,因为它们依赖于对回调的理解。如果您还不能写可维护的回调代码,请继续努力学习!

如果你真的想你的异步代码可以“从上至下阅读”,你可以试试这些美妙的方法。注意,这些功能在不同平台会有兼容性问题,使用前请先调查清楚!

Promise就是一种让你从上至下写回调函数的方法,它鼓励你使用try/catch处理更多类型的错误。

Generator可以让你“暂停”一个函数(而不暂停整个程序),它也能你从上至下写异步函数,但是代价是代码有点复杂难以理解。wat就是使用这个方法。

Async functions是ES7的特性,是生成器和promise更高级的封装,有兴趣自己谷歌一下呗。

就我个人而言,我使用回调函数处理90%的异步代码,当事情变得复杂时,依靠一些库,例如run-parallel或者run-series。我不认为研究回调 vs promise vs 其他什么方法对我来说有什么帮助,最重要的还是保持代码简单,不嵌套,并分成小模块。

无论你选择何种方法,请始终处理每个错误,并保持代码简洁

记住,只有可以防止回调地狱和森林火灾。

原文: http://callbackhell.com/
本文github地址: https://github.com/ssshooter/...

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

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

相关文章

  • ES6-7

    摘要:的翻译文档由的维护很多人说,阮老师已经有一本关于的书了入门,觉得看看这本书就足够了。前端的异步解决方案之和异步编程模式在前端开发过程中,显得越来越重要。为了让编程更美好,我们就需要引入来降低异步编程的复杂性。 JavaScript Promise 迷你书(中文版) 超详细介绍promise的gitbook,看完再不会promise...... 本书的目的是以目前还在制定中的ECMASc...

    mudiyouyou 评论0 收藏0
  • 精读《async/await 是把双刃剑》

    摘要:本周精读内容是逃离地狱。精读仔细思考为什么会被滥用,笔者认为是它的功能比较反直觉导致的。同时,笔者认为,也不要过渡利用新特性修复新特性带来的问题,这样反而导致代码可读性下降。 本周精读内容是 《逃离 async/await 地狱》。 1 引言 终于,async/await 也被吐槽了。Aditya Agarwal 认为 async/await 语法让我们陷入了新的麻烦之中。 其实,笔者...

    2shou 评论0 收藏0
  • JavaScript 异步

    摘要:从最开始的到封装后的都在试图解决异步编程过程中的问题。为了让编程更美好,我们就需要引入来降低异步编程的复杂性。写一个符合规范并可配合使用的写一个符合规范并可配合使用的理解的工作原理采用回调函数来处理异步编程。 JavaScript怎么使用循环代替(异步)递归 问题描述 在开发过程中,遇到一个需求:在系统初始化时通过http获取一个第三方服务器端的列表,第三方服务器提供了一个接口,可通过...

    tuniutech 评论0 收藏0
  • 【译】理解回调和Promise

    摘要:理解回调和原文自工程师博客,传送门这两个概念是编程语言的基本内容。回调地狱就是滥用回调。通常,在回调中,错误作为第一个参数传递。这个具有这两个函数作为参数的回调称为执行程序。到目前为止,我希望我已经让自己了解了回调和。 理解回调和Promise 原文自工程师Fernando Hernandez博客,传送门 这两个概念是Javascript编程语言的基本内容。因为这种语言是在异步编程的...

    liuyix 评论0 收藏0
  • 10.14 百丽集团面试经历

    摘要:此选择器等价于此选择器等价于要匹配含有特定属性但不等于特定值的元素请使用。之前看到的派上了用场。用法返回值集合元素说明匹配给定的属性是以包含某些值的元素。可以包含任意异步操作,而必须是同步函数。 一面 1. 自我介绍 2. jQuery的选择器 jQuery的选择器与css中的选择器很相似,通过使用css中的选择器来选取HTML节点 1. #id 用法: $(#myDiv);...

    bergwhite 评论0 收藏0

发表评论

0条评论

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