[译] 回调地狱——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)中,会确保第 1 行代码已执行完成,并且文件也已加载完毕之后,才开始执行第 2 行代码。但如你所知,JavaScript 并非如此。
什么是回调?
回调(callbacks)只是函数的一种用法的通用称呼,在 JavaScript 中,并没有一个特定的东西叫 “回调”,它仅仅是一个约定好的称呼。
不同于那些立即返回结果的函数,回调函数需要一定的时间来获得结果。
译者注:根据 wiki 上对 callback 的描述,回调分为同步回调和异步回调,这里应该是特指异步回调。详情见:Callback (computer programming)。
This execution may be immediate as in a synchronous callback, or it might happen at a later time as in an asynchronous callback.
“asynchronous(异步)” ,也叫 “async”,表示 “需要耗费一定的时间” 或者 “发生在未来,而不是现在”。
在处理 I/O 时,通常会使用到回调,如下载、读取文件、与数据库交互等。
调用一个普通的函数时,我们可以直接使用其返回值:
var result = multiplyTwoNumbers(5, 10)
console.log(result)
// 控制台打印出 50
而异步回调函数,不会立即返回结果:
var photo = downloadPhoto('http://coolcats.com/cat.gif')
// photo 未定义!
下载 gif 文件可能需要很长的时间,而你肯定不希望程序在下载过程中处于暂停(即 “block(阻塞)”)状态。
你可以把下载完成后需要执行的操作存放在一个函数中,这就是回调函数。把它传递给 downloadPhoto ,当下载完成时, downloadPhoto 会执行这个回调函数(callback,call you back later),并把 error(错误信息)或 photo(图片数据)传递给它。
downloadPhoto('http://coolcats.com/cat.gif', handlePhoto)
function handlePhoto (error, photo) {
if (error) console.error('下载出错!', error)
else console.log('下载完成', photo)
}
console.log('开始下载')
理解回调最大的难点,在于搞清楚程序运行时代码的执行顺序。在这个例子中主要有三个关键点:首先声明了 handlePhoto 函数,然后调用了 downloadPhoto 函数并将 handlePhoto 作为回调函数传递给它,最后 “开始下载” 被打印出来。
注意此时 handlePhoto 还没有被调用,只是创建并作为回调函数传递给了 downloadPhoto ,在 downloadPhoto 完成任务后才会被执行,这取决于网速有多快。
这个例子想要传达两个重要的概念:
- 回调函数 handlePhoto 只是存放操作的一个途径,可以让这些操作在一段时间后(满足了特定条件)才被执行。
- 代码执行的顺序不是按照视觉上的自上而下,而是基于逻辑的完成时机跳跃式触发。
译者注:关于异步回调的执行原理,可以参考 [译] JavaScript 的事件循环。
如何处理回调地狱?
回调地狱的产生源于开发经验的不足,幸运的是想要写好这些代码并不困难。你只要遵循下面三个原则:
1、避免函数嵌套
下面是一段杂乱的代码,使用 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
})
}
如你所见,给函数命名非常简单,却立竿见影:
- 带有描述性含义的函数名,让代码更容易阅读
- 出现异常时,可以在堆栈中查看到一个确切的函数名而不是 “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
}
注意这里把函数声明移到了文件的底部,这得益于函数声明提升(function hoisting)。
2、模块化
这是最重要的一点:人人皆可搞模块(即代码库)。
Anyone is capable of creating modules (aka libraries)
引用(node.js 项目的)Isaac Schlueter 的话:“编写职责单一的小模块,组装起来以实现更大的功能。回调地狱你不去碰它,就不会掉进去。”
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.
让我们从上面的代码中提取出样板代码,拆分成两个文件,把它变成一个模块。我将展示一个模块模式,它既可用于浏览器,也可用于服务端。
新建一个文件叫 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 (并且作为页面的一个外联脚本已加载完成),我们只需要引入(require)这个模块并使用它!
程序的具体代码如下:
var formUploader = require('formuploader')
document.querySelector('form').onsubmit = formUploader.submit
程序仅仅只需要两行代码,而且还有以下好处:
- 对于新的开发者更加容易理解——他们不用深陷于 “被迫通读全部 formuploader 函数”
- formuploader 可以用于其它地方而不需要复制代码,而且也更容易分享到 github 或 npm
3、处理每一个错误
错误有许多类型:语法错误(通常只要运行程序就能被捕获)、运行时错误(程序运行正常但存在一些 bug 会引起逻辑混乱)、平台错误(如无效的文件权限、硬件驱动失效、网络连接异常等)。这一部分主要针对最后一类错误。
前面两个原则可以让你的代码更具可读性,而这个原则,可以让你的代码更具稳定性。
回调函数被定义和分配后,会在后台执行,然后成功完成或者失败中止。任何有经验的开发者都会告诉你:你永远无法预测错误何时会发生,你只能假设它一定会发生。
对于回调函数中错误的处理方式,最流行的是 Node.js 风格:回调函数的第一个参数永远是 “error”。
var fs = require('fs')
fs.readFile('/Does/not/exist', handleFile)
function handleFile (error, file) {
if (error) return console.error('卧槽,出错了', error)
// 正常,可以在代码中使用 `file` 了
}
把第一个参数设置为 error ,是鼓励你记得处理错误的一个简单的约定。如果把它设置为第二个参数,你可能会把代码写成 function handleFile(file){} ,而忽略了错误处理。
编码规范检查工具(Code linters)也可以通过配置来帮助你记得处理回调错误。使用最简单的一个是 standard,你只需要在代码目录中执行 $ standard 命令,它就会把代码中没有处理错误的回调函数全部显示出来。
要点
- 不要嵌套函数,给函数命名并移到外层
- 利用函数声明提升(function hoisting)特性,把函数移到不显眼的位置
- 处理每个回调函数中的 每一个错误,使用检查工具(如 standard)来帮助你更好地完成这个工作
- 创建可重用的函数并放到一个模块中,可以提高代码可读性。代码柯里化也有利于错误处理、编写测试用例、创建一个稳定且文档化的公共 API,此外也便于维护和重构
避免回调地狱的最有效的方法就是 把函数移出去,让程序逻辑更加清晰易懂,新的开发者不用费劲通读所有的函数细节以理解程序意图。
可以先从把函数移到文件尾部开始。然后尝试把它们移到另一个文件里,通过相对路径进行引用,如 require('./photo-helpers.js') 。最后把它们移到一个独立的模块中,像 require('image-resize') 来引用。
这是一些创建模块的实践法则:
- 把一些常用的代码封装成函数
- 当函数(或者一组具有相同主题功能的函数)足够大的时候,把它们移到另一个文件里,使用 module.exports 来暴露接口,通过相对路径进行引用。
- 如果有一些代码可以跨项目使用,给它写个说明文件(readme)、测试用例、 package.json ,并发布到 github 和 npm。
- 一个好的模块应该是轻量的、且聚焦于解决某一个问题
- 一个模块文件不要超过 150 行代码
- 模块文件的目录层级不要嵌套超过一层,如果发生这种情况,可能它就处理太多事情了
- 让更有经验的开发者给你演示下优秀模块的例子,直到你了解它们应该是什么样子的。如果一个模块需要花费超过几分钟的时间去理解它是干嘛的,那就不是一个多么好的模块。
更多阅读
我的 回调更详细的介绍
nodeschool 上的教程
browserify-handbook 编写模块代码的示例
关于 Promise/Generator/ES6
在学习更高级的解决方案之前,请记住,回调函数是 JavaScript 的基础部分(因为它就只是函数),你应该先学会如何阅读和编写回调函数,因为那些更高级的语言特性都是基于对回调函数的理解。如果你还不能编写出可维护的回调函数,请继续学习它!
如果真的想让你的异步代码可以从上往下阅读,这里有一些神奇的东西可以尝试一下。注意,这些可能会引入性能和/或跨平台运行时兼容性问题,所以请考虑你的具体情况。
Promise 是编写异步代码的一种方式,看起来就像是按照代码的顺序从上往下执行,鼓励使用 try/catch 来捕获和处理更多的错误类型。
生成器(Generators)让你可以 “暂停” 个别函数,而不需要暂定整个程序的状态。它有点复杂,想让异步代码按顺序从上往下执行,你需要花费些时间去理解它。可以参考 watt。
Async 函数(async function)是 ES7 的一个提案,在更高级别的语法上进一步封装生成器和 Promise,感兴趣的话可以了解一下。
我个人所编写的异步代码,有 90% 只需要使用到回调函数,如果逻辑比较复杂,我会引入一些工具库,如 run-parallel、run-series。我不认为使用回调函数、Promise 或者其它的第三方库有什么不同,最重要的是保持代码简单、不要嵌套、拆分成小模块。
无论你选择哪种方式,请始终 处理每一个错误 并 保持代码简单。
记住,只有你可以防止回调地狱和森林火灾
你可以在这个 github 上查看相关源码。
[译] 回调地狱——JavaScript异步编程指南的更多相关文章
- Javascript异步编程之二回调函数
上一节讲异步原理的时候基本上把回掉函数也捎带讲了一些,这节主要举几个例子来具体化一下.在开始之前,首先要明白一件事,在javascript里函数可以作为参数进行传递,这里涉及到高阶函数的概念,大家可以 ...
- JavaScript异步编程原理
众所周知,JavaScript 的执行环境是单线程的,所谓的单线程就是一次只能完成一个任务,其任务的调度方式就是排队,这就和火车站洗手间门口的等待一样,前面的那个人没有搞定,你就只能站在后面排队等着. ...
- 深入解析Javascript异步编程
这里深入探讨下Javascript的异步编程技术.(P.S. 本文较长,请准备好瓜子可乐 :D) 一. Javascript异步编程简介 至少在语言级别上,Javascript是单线程的,因此异步编程 ...
- Func-Chain.js 另一种思路的javascript异步编程解决方案
本文转载自:https://www.ctolib.com/panruiplay-func-chain.html Func-Chain.js 另一种思路的javascript异步编程,用于解决老式的回调 ...
- JavaScript异步编程的主要解决方案—对不起,我和你不在同一个频率上
众所周知(这也忒夸张了吧?),Javascript通过事件驱动机制,在单线程模型下,以异步的形式来实现非阻塞的IO操作.这种模式使得JavaScript在处理事务时非常高效,但这带来了很多问题,比如异 ...
- javascript异步编程的前世今生,从onclick到await/async
javascript与异步编程 为了避免资源管理等复杂性的问题, javascript被设计为单线程的语言,即使有了html5 worker,也不能直接访问dom. javascript 设计之初是为 ...
- JavaScript异步编程(2)- 先驱者:jsDeferred
JavaScript当前有众多实现异步编程的方式,最为耀眼的就是ECMAScript 6规范中的Promise对象,它来自于CommonJS小组的努力:Promise/A+规范. 研究javascri ...
- Javascript异步编程方法总结
现在我们有三个函数,f1, f2, f3 按正常的思路我们会这样写代码: function f1 (){}; function f2 (){}; function f3 (){}; //在这里调用函数 ...
- Promises与Javascript异步编程
Promises与Javascript异步编程 转载:http://www.zawaliang.com/2013/08/399.html 在如今都追求用户体验的时代,Ajax应用真的是无所不在.加上这 ...
随机推荐
- 使用composer出现 Cannot find module (SNMPv2-TC) 等错误的解决方法
Cannot find module (SNMPv2-TC): At line 10 in /usr/share/snmp/mibs/UCD-DLMOD-MIB.txt Cannot find mod ...
- Android应用开发之使用Socket进行大文件断点上传续传
http://www.linuxidc.com/Linux/2012-03/55567.htm http://blog.csdn.net/shimiso/article/details/8529633 ...
- css伪类元素:after 的多功能用法——任意大小的底边框
需求用法出现的背景: 由于项目UI的优化,项目中所有tab导航选中的状态都是统一样式书写的,之前都是用的border-bottom,新的需求如果用以前的本办法就是定位一个选中边框在底部,但是涉及的模板 ...
- 中矿新生赛 H 璐神看岛屿【BFS/DFS求联通块/连通块区域在边界则此连通块无效】
时间限制:C/C++ 1秒,其他语言2秒空间限制:C/C++ 32768K,其他语言65536K64bit IO Format: %lld 题目描述 璐神现在有张n*m大小的地图,地图上标明了陆地(用 ...
- Python的网络编程[1] -> FTP 协议[2] -> 使用 ftplib 建立 FTP 客户端
使用 ftplib 建立 FTP 客户端 用于建立FTP Client,与 pyftplib 建立的 Server 进行通信. 快速导航 1. 模块信息 2. 建立 FTP 客户端 1. 模块信息 1 ...
- Nginx的proxy_pass及upstream的小型负载均衡
proxy_pass Nginx的proxy_pass将请求代理到其他的后端服务器.例如 listen 9999; server_name wyc.com; location /test/aaa { ...
- (转)Unity3d通过Action注册事件,回调方法
http://www.cnblogs.com/jisi5789/archive/2013/04/22/3036589.html using UnityEngine; namespace Liulala ...
- apache去掉目录浏览
apache去掉目录浏览 apache默认开启目录浏览的,这样大大降低了我们网站的安全,下面是关闭浏览目录: 要禁止 Apache 显示目录结构列表,只需将 Option 中的 Indexes 去掉即 ...
- Word中如何公式居中标号右对齐
1.鼠标居中 2.插入一行三列表格 3.选中第一个表格,右键-表格属性-单元格-选项:然后回到单元格设置垂直居中,宽度为15%,同理第三个单元格,不过中间单元格也要设置,宽度为70%,这个word没有 ...
- Object 类中的 equals方法
1 相等与同一 如果两个对象具有相同的类型以及相同的属性值,则称这两个对象相等.如果两个引用对象指的是同一个对像,则称这两个变量同一.Object类中定义的equals 函数原型为:public bo ...