JavaScript是一种奇怪的语言。偶尔,你不得不处理一个回调,而这个回调又在另一个回调中,又在另一个回调中。
人们亲切地称这种模式为回调地狱。
它看起来有点像这样。
firstFunction(args, function() {
这就是JavaScript的魅力所在。看到嵌套的回调是令人匪夷所思的,但我不认为这是一个 "地狱"。如果你知道该如何处理,"地狱 "是可以管理的。
关于回调
我假设你知道什么是回调,如果你正在阅读这篇文章。如果你不知道,请在继续阅读之前先阅读这篇文章,了解回调的介绍。在那里,我们谈论了什么是回调,以及为什么在JavaScript中使用它们。
回调地狱的解决方案
回调地狱有四个解决方案:
- 写注释
- 将函数分割成更小的函数
- 使用承诺
- 使用Async/await
在我们深入研究解决方案之前,让我们一起构建一个回调地狱。为什么?因为看到firstFunction、secondFunction和thirdFunction太抽象了。我们想把它具体化。
构建一个回调地狱
让我们想象一下,我们正试图做一个汉堡包。为了做一个汉堡,我们需要经历以下步骤:
- 获取原料(我们假设这是一个牛肉汉堡包)
- 烹饪牛肉
- 获取汉堡包
- 把煮好的牛肉放在包子中间
- 端出汉堡
如果这些步骤是同步的,你将会看到一个类似于这样的函数:
const makeBurger = () => {
const burger = makeBurger();
然而,在我们的场景中,假设我们不能自己制作汉堡。我们必须指示一个帮手来制作汉堡的步骤。在我们指示完助手后,我们必须等待助手完成,然后再开始下一个步骤。
如果我们想在JavaScript中等待什么,我们需要使用回调。为了制作汉堡,我们必须先得到牛肉。我们只有在得到牛肉之后才能烹饪牛肉。
const makeBurger = () => {
要烹饪牛肉,我们需要把牛肉传给cookBeef函数。否则,就没有东西可做了然后,我们必须等待牛肉被煮熟。
一旦牛肉被煮熟了,我们就可以得到包子:
const makeBurger = () => {
拿到包子后,我们需要把肉饼放在包子中间。这就是一个汉堡的形成:
const makeBurger = () => {
最后,我们可以端出汉堡了!但我们不能从makeBurger返回汉堡,因为它是异步的。我们需要接受一个回调以提供汉堡:
const makeBurger = nextStep => {
// Make and serve the burger
(我在制作这个回调地狱的例子时很开心)。
回调地狱的第一个解决方案:写评论
makeBurger的回调地狱很容易理解。我们可以阅读它。它只是看起来不好看。
如果你是第一次读makeBurger,你可能会想 "为什么我们需要这么多回调来制作一个汉堡?这没有意义!"。
在这种情况下,你会想留下注释来解释你的代码:
// Makes a burger
const makeBurger = nextStep => {
现在,当你看到回调地狱时,你不会再想 "wtf?!",而是了解了为什么必须这样写。
回调地狱的第二个解决方案。将回调分成不同的函数
我们的回调地狱的例子已经是一个例子了。让我给你看看一步步的命令式代码,你就会明白为什么。
对于getBeef,我们的第一个回调,我们必须到冰箱里去拿牛肉。厨房里有两个冰箱。我们需要去右边的冰箱:
const getBeef = nextStep => {
要煮牛肉,我们需要把牛肉放进烤箱;把烤箱调到200度,然后等待20分钟:
const cookBeef = (beef, nextStep) => {
现在想象一下,如果你要在makeBurger中写出这些步骤......你可能会因为代码量太大而晕倒!
关于将回调分割成小函数的具体例子,你可以阅读我的回调文章中的这一小部分。
回调地狱的第三个解决方案。使用承诺
我将假设你知道什么是承诺。如果你不知道,请阅读这篇文章。
承诺可以使回调地狱更容易管理。你将拥有这样的代码,而不是你在上面看到的嵌套代码:
const makeBurger = () => {
// Make and serve burger
如果你利用诺言的单参数风格,你可以把上面的代码调整成这样。
const makeBurger = () => {
// Make and serve burger
更容易阅读和管理。
但问题是,你如何将基于回调的代码转换成基于承诺的代码。
将回调转换为承诺
为了将回调转换为承诺,我们需要为每个回调创建一个新的承诺。当回调成功时,我们可以解决这个承诺。或者,如果回调失败,我们可以拒绝该承诺。
const getBeefPromise = _ => {
return new Promise((resolve, reject) => {
const cookBeefPromise = beef => {
return new Promise((resolve, reject) => {
在实践中,回调可能已经为你写好了。如果你使用Node,每个包含回调的函数将有相同的语法:
- 回调将是最后一个参数
- 回调将总是有两个参数。而且这些参数的顺序是一样的。(首先是错误,其次是你感兴趣的东西)。
// The function that’s defined for you
// How you use the function
如果你的回调具有相同的语法,你可以使用像ES6 Promisify 或Denodeify(去节点化)这样的库,将回调变成一个承诺。如果你使用Node v8.0及以上版本,你可以使用util.promisify。
这三种方式都可以。你可以选择任何库来工作。不过,每种方法之间都有细微的差别。我将让你去查看他们的文档,了解如何操作。
回调地狱的第四个解决方案:使用异步函数
要使用异步函数,你首先需要知道两件事:
有了异步函数,你就可以把makeBurger写得好像它又是同步的了
const makeBurger = async () => {
// Make and serve burger
这里我们可以对makeBurger做一个改进。你可能可以同时得到两个getBuns和getBeef的帮助器。这意味着你可以用Promise.all来等待他们两个。
const makeBurger = async () => {
// Make and serve burger
(注意:你也可以用Promise做同样的事情......但是语法没有像ync/await函数那样好,那样清晰)。
总结
回调地狱并不像你想象的那么可怕。有四种简单的方法来管理回调地狱:
- 写注释
- 将函数分割成更小的函数
- 使用承诺
- 使用Async/await