![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLicmbw5SM1UmYhV2N5UDOjRGOwAjZllzM2UWM1gTM1UGMwEDZk9CX0JXZ252bj91Ztl2Lc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
学习异步JS的时候最头大的就是看到一层一层嵌套的函数回调。每次看到就一万个wtf/wth在心中飘过。当然,不仅我一个人这样。外国的同行称之为:Callback Hell---回调函数地狱。如果谷歌一下,你还会发现有一个网站就是以此为域名的:http://callbackhell.com。好事的我就进去这个网站看了一翻,里面详细解释了callback hell并给出解决方案和建议。并提出模块化开发理论,讲解其起源及开发原则。让一直迷糊进行模块化开发的我有了醍醐灌顶之感。现翻译过来与诸君共享。
原文链接:
Callback Hellcallbackhell.com
什么是“回调函数地狱”?
异步JavaScript、或使用回调的JavaScript让人很难直观理解。许多代码看起来是这样的:
fs
看到没,这段代码乍一看就像是一个金字塔▲,还有一个有一个的”]}“跟在结尾处?恶(⊙﹏⊙)!这就是被亲切称之为”回调地狱-Callback Hell“的东西。
回调地狱的成因是程序员试图以程序执行的”从上而下“的方式来编写代码。很多人会犯这个错误!我们在其他语言(例如C,Ruby或Python)中,可以期望前面一行代码跑完才跑下一行代码,直至代码结束。而正如你接下来要学到的,JavaScript并不是这样。
什么是回调函数?
”回调函数“只是一种约定俗成的说法,指的就是”使用JavaScript函数/方法(functions)“。虽然看起来这个名词很高级晦涩,但其实指代的就是“使用JS函数”。而与大多数的JS函数有所不同的是:它不会立刻返回一个值,而是需要一些时间才能得出一个结果。”异步“(async)的意思也仅仅就是”花费一点时间“或者”将来发生,不是当下发生“。通常,回调函数仅在执行I/O时使用,比如下载文件、读取文件、与数据库进行交互之类的时候。
调用普通函数时,你可以使用它的返回值:
var
而调用异步的回调函数时,他不会立刻有一个返回值:
var
在这个例子中,下载cat.gif文件可能需要很长时间,在这段时间里,你并不希望你的代码就此暂停(又称为“阻塞”)来等待下载完成。
于是我们将下载完以后需要执行的代码放到一个function里。这样就是一个回调函数了!把这个回调函数放到downloadPhoto方法里,这样等到下载完成时,就会把.git图片传给回调函数并执行该回调函数了。
downloadPhoto
人们理解回调函数时最大的难点就是理解他的执行顺序。在上面这个例子中,主要发生了三件事。第一声明了handlePhoto函数,然后downloadPhoto函数被调用并将handlePhoto作为回调函数,最后打印了“Download started”。
注意hanlePhoto一开始并没有被调用,只是进行了声明和作为参数传递给了downloadPhoto。要等到downloadPhoto执行完才会执行handlePhoto。而downloadPhoto执行的时间长短取决于你的网速,也许会是很长很长一段时间。
举上述这个例子是为了说明两个重要的概念:
- handlePhoto回调函数指的就是等一段时间后才执行的代码。
- 带有回调函数的代码的执行顺序并不是按着我们阅读的从上到下的顺序,而是会跳来跳去。这里执行完了,就去执行这里、那里执行完了,就去执行那里。
如何走出回调函数地狱?
回调函数地狱是由不良的编码习惯引起的。 幸运的是,编写更好的代码并不难!
你只需遵守下面的三条原则:
1.让代码浅显易懂
这里有一个混乱的AJAX请求的代码:
var
这段代码有两个匿名函数。让我们给他们起个名字:
var
如上可见,给函数进行命名非常简单,且有一些立竿见影的好处:
- 赋予方法描述性的名字可以提高代码的易读性。
- 当发生异常时,可以在堆栈中跟踪查询到这个实际发生异常的函数名,而不是一个没有名字的“匿名函数”。
- 可以移动、修改函数并根据名称进行引用
接下来我们将函数调用移到程序顶层:
document
注意这里要感谢“函数提升”机制,让我们可以把函数的声明放这在函数调用的下面。
2.模块化
这里有很重要的一点需要说明:任何人都可以创建模块(又称:库)。这里引用Isaac Schlueter 的一句话:“编写每次完成一件事的小代码块,然后把他们拼到一起完成一件更大的事。只要你不去编写有多层回调的代码,你就不会走进回调函数地狱。”
让我们将上面的代码进行提取、拆分来将其转换为模块。我将展示一个适用于客户端或服务端(或同时适用于两者的)的模块模式:
这是一个名为formuploader.js的新文件,其包含之前的两个函数:
module
module.exports是源于node.js的用法,可以跑在node、Electron以及启用了browserify的浏览器上。我非常喜欢这种形式的模块,因为它可以跑在任何地方、易于理解且不需要复杂的配置文件或脚本。
现在我们有了formuploader.js(在浏览器读到时,它以脚本标签的形式加载到了页面中),我们只需要require并使用它即可! 这是我们的代码现在的样子:
var
现在我们的代码只有两行了,这样的代码有以下好处:
- 更易于理解--无需把整个formuploader的代码都读一遍。
- formuploader可以被复用,并可以被共享到github或者npm上被更广泛地分享。
3.单独处理每一个报错
报错的类型有多种:由程序员引起的语法错误(通常是在首次尝试运行程序时报出的错误),由程序员引起的运行时报出的错误(代码已运行但有会引出问题的bug),平台错误比如没有访问权限,硬盘驱动故障,无网络连接。本节仅用于解决最后一类错误。
前面讲过的两条原则都是为了使你的代码提高易读性,现在要讲的这条原则是为了让你的代码更稳定。按照定义,在处理回调函数时就是处理一些被分派的任务,先在后台执行一些操作,然后成功完成或由于报错而中止。 即使再有经验的开发者也无法说得准这些错误什么时候会出现,所以我们要假设它在任何时候都可能发生。
回调函数里最常见的处理错误的方式是Node.js这种风格的,把第一个参数永远预留为错误信息error的方式。
var
把第一个参数作为错误信息是一种约定俗成的简单用法,这可以使我们记得要处理错误信息。如果作为第二个参数,我们可能会编写类似handleFile(file)的代码,从而忘记处理错误信息。
我们也可以配置code linter来提醒我们去处理错误信息。最简单的方式是用standard。我们所要做的就是在代码文件夹中运行$ standard,它将显示代码中的每个未处理错误信息的回调函数。
小结
- 不要函数嵌套。给函数命名并将他们的调用放到项目顶层。
- 函数声明时利用好函数提升机制。把代码块放在“后面”。
- 每个回调函数都注意处理他的错误信息。善用linter中的standard。
- 编写可复用代码模块。把大的代码块拆成小的代码块。从而更好地处理错误信息、做测试、并强制我们开发更稳定的、有文档的公共API。
避免回调函数地狱最重要的一点就是把函数进行提取来让代码流更易读。从而避免后续开发者要把整段代码都细细读过才能明白这段代码究竟在做什么。
您可以先将函数移到文件的底部,然后逐步将其移至像这样的相对路径的require('./ photo-helpers.js'),最终将其移至独立模块,例如require('image-resize')。
这是创建模块时的一些经验法则:
- 先着手把多次复用的代码移到一个函数里
- 如果一段代码已经很长了,可以考虑把他们移到单独文件并通过module.exports暴露接口。并通过相对路径的require进行加载。
- 如果你有一段代码多个项目都可以复用,试着编写readme指南、测试用例以及package.json并将其发布到github和npm上。这样做好处多多!
- 一个好的模块一定满足两个条件:1小,2只专注于解决一个问题。
- 一个模块中的每个文件不应该超过150行。
- 一个模块不应该有两层以上的文件夹结构。如果是这样,说明这个模块完成太多事情了。
- 向其它有经验的开发者请教,看一些好的模块直到明白好的模块代码长什么样子。如果你要花一分钟以上的时间弄明白一个模块再做什么,就说明这可能不是什么好例子。
看了这篇文章,回想起以前编写的一些模块,就是提取出来以后,结果复用性并不高。其实就是因为一次做了太多事,稍有变化,就要摞加更多代码来进行自定义。这样想来,那时候的代码可以再进行提取,提取到最小单位,就可以避免这种问题了。