(渣渣小怪兽翻译,如果看翻译不开心可以看->)
js是一门强大语言。有时候,你必须处理另一个回调中的回调,而这个回调这是另一个回调中的回调。
人们形象的描述这种模式为回调地狱。
它有点像这样:
firstFunction(args, function() { secondFunction(args, function() { thirdFunction(args, function() { // And so on… }); });});复制代码
这是js中的回调。当你看到这样的嵌套回调, 它可能令你的大脑难以置信,但我不认为这是“地狱”.这个“地狱”可以更好的管理,如果你知道怎么处理它的话。
关于回调
我假设当你在阅读这篇文章的时候你知道回调的概念.如果你不知道, 请阅读这篇, 在我们继续往下走之前这篇文章会介绍什么是回调。在那里,我们讨论回调是什么以及为什么在JavaScript中使用它们。
回调的处理方案
这是四个处理回调地狱的方案
- 写注释
- 拆分函数成为更小的函数
- 使用Promise
- 使用Async/await
在我们拆分讲解这个解决方案之前, 让我们一起构造一个回调地狱. 为什么?因为它真的太抽象了,当我们看到上面的例子: firstFunction, secondFunciton 和 thirdFunction.我们让构造一个真是的回调, 让我们的例子更具体。
构造一个回调地狱
让我们想象, 现在我们要做一个汉堡.为了制作一个汉堡, 我们需要执行以下步骤来达到目的:
- 获取配料
- 烹饪牛肉
- 获取汉堡的面包
- 把煮好的牛肉放在面包之间
- 供应汉堡
如果这些步骤都是同步的, 你将看到一个函数像下面这样:
const makeBurger = () => { const beef = getBeef(); const patty = cookBeef(beef); const buns = getBuns(); const burger = putBeefBetweenBuns(buns, beef); return burger;};const burger = makeBurger();serve(burger);复制代码
然而, 在我们的步骤中,我们不能自己制作汉堡。我们必须指导助手制作汉堡的步骤。在我们指示助手之后,我们必须等待助手完成步骤,然后才开始下一步。
如果我们想在js中等待一会再执行, 我们就需要使用回调。为了制作汉堡, 我们不得不先获取牛肉。我们只能在获取牛肉后再煮牛肉。
const makeBurger = () => { getBeef(function(beef) { // We can only cook beef after we get it. });};复制代码
为了煮牛肉, 我们需要传递牛肉进入cookBeef函数内.否则我们将没有东西煮。因此我们不得不等待牛肉煮熟.
一旦牛肉煮好了,我们就要获取面包
const makeBurger = () => { getBeef(function(beef) { cookBeef(beef, function(cookedBeef) { getBuns(function(buns) { // Put patty in bun }); }); });};复制代码
在我们获取面包后,我们需要放肉饼在两个面包之间。这就是汉堡形成的地方.
const makeBurger = () => { getBeef(function(beef) { cookBeef(beef, function(cookedBeef) { getBuns(function(buns) { putBeefBetweenBuns(buns, beef, function(burger) { // Serve the burger }); }); }); });};复制代码
最终, 我们可以提供汉堡了。但是我们不能从makeBurger中返回burger, 因为这是异步的。我们需要用一个回调去接收汉堡.
const makeBurger = nextStep => { getBeef(function (beef) { cookBeef(beef, function (cookedBeef) { getBuns(function (buns) { putBeefBetweenBuns(buns, beef, function(burger) { nextStep(burger) }) }) }) })}// Make and serve the burgermakeBurger(function (burger) => { serve(burger)})复制代码
制作这个回调的例子很有趣啊
解决方案1: 写注释
这个makeBurger回调是简单易于理解。我们可以读懂。它只是有一些不好看。
如果你第一次读到makeBurger这个函数, 你可能在想“为什么我们需要这么多回调才能制作汉堡呢?这没有意义!”
在这种情况下,您需要留下注释来解释您的代码。
// Makes a burger// makeBurger contains four steps:// 1. Get beef// 2. Cook the beef// 3. Get buns for the burger// 4. Put the cooked beef between the buns// 5. Serve the burger (from the callback)// We use callbacks here because each step is asynchronous.// We have to wait for the helper to complete the one step// before we can start the next stepconst makeBurger = nextStep => { getBeef(function(beef) { cookBeef(beef, function(cookedBeef) { getBuns(function(buns) { putBeefBetweenBuns(buns, beef, function(burger) { nextStep(burger); }); }); }); });};复制代码
现在,不会是在想“WTF”, 当你看到这个回调地狱, 你可以理解为什么要用这个方式去写它了。
解决方案2:将回调拆分为不同的函数
我们的回调地狱的例子已经是一个很棒拆分的例子。让我给你一步步的拆分代码, 就会明白为什么我这样说了。
拿getBeef, 我们的第一个回调来说, 我们为了拿到牛肉不得不先去冰箱。厨房里有两个冰箱,我们需要去右手边的冰箱拿牛肉。
const getBeef = nextStep => { const fridge = leftFright; const beef = getBeefFromFridge(fridge); nextStep(beef);};复制代码
为了烹饪牛肉,我们需要把牛肉放进烤箱里, 把烤箱开到200度, 并且等20分钟
const cookBeef = (beef, nextStep) => { const workInProgress = putBeefinOven(beef); setTimeout(function() { nextStep(workInProgress); }, 1000 * 60 * 20);};复制代码
现在想象一下, 如果你不得不写每个步骤在makeBurger ,那么这个函数就会非常的庞大。
有关将回调拆分为较小函数的具体示例,您可以在我的回调文章中阅读这一小部分。
解决方案3: 使用Promise
我猜你应该知道什么是Promise.如果你不知道, 请先阅读这一篇
Promise能让回调地狱更易于管理.而不是像上面的嵌套回调.你将看到像这样:
const makeBurger = () => { return getBeef() .then(beef => cookBeef(beef)) .then(cookedBeef => getBuns(beef)) .then(bunsAndBeef => putBeefBetweenBuns(bunsAndBeef));};// Make and serve burgermakeBurger().then(burger => serve(burger));复制代码
如果你更倾向于单参数风格的promise, 你能把上面的例子转换成这样
const makeBurger = () => { return getBeef() .then(cookBeef) .then(getBuns) .then(putBeefBetweenBuns);};// Make and serve burgermakeBurger().then(serve);复制代码
更易读和管理。
但是问题是, 如何把回调地狱转换成Promise?
把回调转成Promise
为了转成Promise, 我们需要为每个回调先new一个Promise.当这个回调成功执行,那么给这个Promise使用resolve返回。或当这个回调失败或者抛出错误的时候,我们就要使用reject。
const getBeefPromise = _ => { const fridge = leftFright; const beef = getBeefFromFridge(fridge); return new Promise((resolve, reject) => { if (beef) { resolve(beef); } else { reject(new Error(“No more beef!”)); } });};const cookBeefPromise = beef => { const workInProgress = putBeefinOven(beef); return new Promise((resolve, reject) => { setTimeout(function() { resolve(workInProgress); }, 1000 * 60 * 20); });};复制代码
在我们的练习中,可能已经为您编写了回调函数。如果使用Node,则包含回调的每个函数将具有相同的语法:
- 回调函数将是最后一个参数
- 这个回调函数将一直有两个参数。这些参数都有相同的顺序(error是第一个, 随后是你感兴趣的任何东西)
// The function that’s defined for youconst functionName = (arg1, arg2, callback) => { // Do stuff here callback(err, stuff);};// How you use the functionfunctionName(arg1, arg2, (err, stuff) => { if (err) { console.error(err); } // Do stuff});复制代码
如果你的回调已经有了相同的语法, 你可以使用像ES6 Promisify or Denodeify 把回调转换成Promise.如果你使用Node v8.0或者之上, 你可以使用util.promisify.
他们三个都可以使用。你可以选择任意一个库来使用。不过,每种方法之间都有细微的差别。我建议你查看它的文档,了解方法。
解决方案4: 使用异步函数(async/await)
为了使用异步函数, 你首先需要知道两件事。
- 如何把callback转换为Promise(就是我们解决方案三中说的内容)
- 怎么使用异步函数(如果你需要帮助,你可以读一下这篇)
使用异步函数,您可以编写makeBurger,就像写同步一样!
const makeBurger = async () => { const beef = await getBeef(); const cookedBeef = await cookBeef(beef); const buns = await getBuns(); const burger = await putBeefBetweenBuns(cookedBeef, buns); return burger;};// Make and serve burgermakeBurger().then(serve);复制代码
我们可以在这里对makeBurger进行一项改进。 你可以同时获得两个getBuns和getBeef的助手。 这意味着您可以使用Promise.all语法, 然后使用await。
const makeBurger = async () => { const [beef, buns] = await Promise.all(getBeef, getBuns); const cookedBeef = await cookBeef(beef); const burger = await putBeefBetweenBuns(cookedBeef, buns); return burger;};// Make and serve burgermakeBurger().then(serve);复制代码
(注意: 你可以在Promise做到相同的效果, 但是它的语法并不是很棒, 不像async/await那么清晰。)
总结
回调地狱并不是像你想象中的那么可怕.这里有四种方式管理回调地狱。
- 写注释
- 切分函数为更小的函数
- 使用Promise
- 使用async/await