有人可以给出一个明确的定义以及一个简单的例子来解释什么是"回调地狱"对于不了解JavaScript和node.js的人?
当(在什么样的设置中)"回调地狱问题"发生?
为什么会发生?
"回调地狱"总是与异步计算有关?
或者可以"回调地狱"也出现在单线程应用程序中?
我参加了Coursera的Reactive课程,Erik Meijer在他的一个讲座中说RX解决了回调地狱的问题"。我问什么是"回调地狱"在Coursera论坛上但我得不到明确的答案。
解释"回调地狱"举一个简单的例子,你能否说明RX如何解决"回调地狱问题"在那个简单的例子上?
答案 0 :(得分:113)
这个问题有一些Javascript回调地狱的例子:How to avoid long nesting of asynchronous functions in Node.js
Javascript中的问题是"冻结"计算并有其余的"其余的"执行后者(异步)是将"其余部分放入"在回调中。
例如,假设我想运行如下代码:
x = getData();
y = getMoreData(x);
z = getMoreData(y);
...
如果现在我想让getData函数异步,会发生什么?这意味着我有机会在等待它们返回值时运行其他代码?在Javascript中,唯一的方法是使用continuation passing style重写触及异步计算的所有内容:
getData(function(x){
getMoreData(x, function(y){
getMoreData(y, function(z){
...
});
});
});
我不认为我需要说服任何人这个版本比前一版本更丑。 : - )
当你的代码中有很多回调函数时!你在代码中使用它们的次数越多越难,当你需要做循环,try-catch块和类似的事情时它会变得特别糟糕。
例如,据我所知,在JavaScript中,执行一系列异步函数的唯一方法是在前一次返回之后运行一个异步函数,这是使用递归函数。你不能使用for循环。
// we would like to write the following
for(var i=0; i<10; i++){
doSomething(i);
}
blah();
相反,我们可能需要写完:
function loop(i, onDone){
if(i >= 10){
onDone()
}else{
doSomething(i, function(){
loop(i+1, onDone);
});
}
}
loop(0, function(){
blah();
});
//ugh!
我们在StackOverflow上询问如何做这类事情的问题数量证明了它是多么令人困惑:)
这是因为在JavaScript中,延迟计算以使其在异步调用返回后运行的唯一方法是将延迟代码放在回调函数中。您不能延迟以传统同步样式编写的代码,因此您最终会在任何地方使用嵌套回调。
异步编程与并发性有关,而单线程与并行性有关。这两个概念实际上并不是一回事。
您仍然可以在单线程上下文中使用并发代码。事实上,JavaScript,回调地狱的女王,是单线程的。
What is the difference between concurrency and parallelism?
我对RX一无所知,但通常通过在编程语言中添加对异步计算的本机支持来解决这个问题。实现可以变化,包括:异步,生成器,协同程序和callcc。
在Python中,我们可以使用以下内容实现上一个循环示例:
def myLoop():
for i in range(10):
doSomething(i)
yield
myGen = myLoop()
这不是完整的代码,但我们的想法是&#34; yield&#34;暂停我们的for循环,直到有人调用myGen.next()。重要的是我们仍然可以使用for循环编写代码,而不需要编写逻辑&#34;内部&#34;就像我们必须在递归loop
函数中做的那样。
答案 1 :(得分:29)
回答这个问题:请问你能否说明RX如何在这个简单的例子中解决“回调地狱问题”?
魔法是flatMap
。我们可以在Rx中为@ hugomg的例子编写以下代码:
def getData() = Observable[X]
getData().flatMap(x -> Observable[Y])
.flatMap(y -> Observable[Z])
.map(z -> ...)...
就像你正在编写一些同步FP代码,但实际上你可以通过Scheduler
使它们异步。
答案 2 :(得分:18)
解决Rx如何解决回调地狱的问题:
首先让我们再次描述回调地狱。
想象一下我们必须做一个http来获得三个资源 - 人,行星和星系。我们的目标是找到人生活的星系。首先,我们必须得到人,然后是行星,然后是星系。这是三次异步操作的三次回调。
select format(date, '%m-%d') as mmdd,
sum(case when year(date) = 2016 then tickets end) as tickets_2016,
sum(case when year(date) = 2017 then tickets end) as tickets_2017
from t
group by format(date, '%m-%d');
每个回调都是嵌套的。每个内部回调都依赖于其父级。这导致了厄运的金字塔&#34; 回调地狱的风格。代码看起来像&gt;登录。
要在RxJs中解决这个问题,你可以这样做:
getPerson(person => {
getPlanet(person, (planet) => {
getGalaxy(planet, (galaxy) => {
console.log(galaxy);
});
});
});
使用getPerson()
.map(person => getPlanet(person))
.map(planet => getGalaxy(planet))
.mergeAll()
.subscribe(galaxy => console.log(galaxy));
AKA mergeMap
运算符,您可以更简洁:
flatMap
正如您所看到的,代码是扁平化的,并且包含一个方法调用链。我们没有厄运的金字塔&#34;。
因此,避免了回调地狱。
如果您想知道, promises 是避免回调地狱的另一种方式,但承诺是渴望,而不是 lazy ,如observables和(一般来说)你不能轻易取消它们。
答案 3 :(得分:14)
回调地狱是在异步代码中使用函数回调变得模糊或难以遵循的任何代码。通常,当存在多个间接级别时,使用回调的代码可能变得更难以遵循,更难以重构,并且更难以测试。由于传递了多层函数文字,代码气味是多级缩进。
当行为具有依赖关系时,通常会发生这种情况,即A必须在B必须发生在C之前发生。然后你得到这样的代码:
a({
parameter : someParameter,
callback : function() {
b({
parameter : someOtherParameter,
callback : function({
c(yetAnotherParameter)
})
}
});
如果您的代码中存在大量行为依赖性,那么它很快就会变得麻烦。特别是如果它分支...
a({
parameter : someParameter,
callback : function(status) {
if (status == states.SUCCESS) {
b(function(status) {
if (status == states.SUCCESS) {
c(function(status){
if (status == states.SUCCESS) {
// Not an exaggeration. I have seen
// code that looks like this regularly.
}
});
}
});
} elseif (status == states.PENDING {
...
}
}
});
这不会做。我们怎样才能使得异步代码以确定的顺序执行而不必传递所有这些回调?
RX是&#39;反应性扩展的缩写&#39;。我还没有使用它,但谷歌搜索表明它是一个基于事件的框架,这是有道理的。 事件是使代码按顺序执行而不会产生脆弱耦合的常见模式。你可以让C听这个事件&#39; bFinished&#39;只有在B被称为“a完成”后才会发生这种情况。然后,您可以轻松添加额外的步骤或扩展此类行为,并且只需在测试用例中广播事件,就可以轻松测试您的代码按顺序执行。
答案 4 :(得分:1)
回叫地狱意味着您处于另一个回调内部的回调中,并且将转到第n个调用,直到您的需求没有完全满足为止。
让我们通过使用set timeout API通过一个伪造的ajax调用示例来理解,假设我们有一个食谱API,我们需要下载所有食谱。
<body>
<script>
function getRecipe(){
setTimeout(()=>{
const recipeId = [83938, 73838, 7638];
console.log(recipeId);
}, 1500);
}
getRecipe();
</script>
</body>
在上面的示例中,计时器超时1.5秒后,内部回调代码将执行,换句话说,通过我们的虚假ajax调用,所有配方都将从服务器上下载。现在我们需要下载特定的配方数据。
<body>
<script>
function getRecipe(){
setTimeout(()=>{
const recipeId = [83938, 73838, 7638];
console.log(recipeId);
setTimeout(id=>{
const recipe = {title:'Fresh Apple Juice', publisher:'Suru'};
console.log(`${id}: ${recipe.title}`);
}, 1500, recipeId[2])
}, 1500);
}
getRecipe();
</script>
</body>
要下载特定的配方数据,我们在第一个回调中编写了代码并传递了配方ID。
现在,我们需要下载ID为7638的同一发布者的所有食谱。
<body>
<script>
function getRecipe(){
setTimeout(()=>{
const recipeId = [83938, 73838, 7638];
console.log(recipeId);
setTimeout(id=>{
const recipe = {title:'Fresh Apple Juice', publisher:'Suru'};
console.log(`${id}: ${recipe.title}`);
setTimeout(publisher=>{
const recipe2 = {title:'Fresh Apple Pie', publisher:'Suru'};
console.log(recipe2);
}, 1500, recipe.publisher);
}, 1500, recipeId[2])
}, 1500);
}
getRecipe();
</script>
</body>
要满足我们的需求,即下载发布者名称suru的所有食谱,我们在第二个回叫内编写了代码。很明显,我们编写了一个回调链,称为回调地狱。
如果要避免回调地狱,可以使用Promise(这是js es6功能),每个promise都会执行一个回调,当promise满时,将调用该回调。 promise回调有两个选项,即已解决或拒绝。假设您的API调用成功,则可以调用resolve并通过 resolve 传递数据,您可以使用 then()来获取此数据。但是,如果您的API失败,则可以使用拒绝,请使用 catch 来捕获错误。请记住,承诺始终使用然后进行解决,并抓住进行拒绝
让我们使用promise解决上一个回调地狱问题。
<body>
<script>
const getIds = new Promise((resolve, reject)=>{
setTimeout(()=>{
const downloadSuccessfull = true;
const recipeId = [83938, 73838, 7638];
if(downloadSuccessfull){
resolve(recipeId);
}else{
reject('download failed 404');
}
}, 1500);
});
getIds.then(IDs=>{
console.log(IDs);
}).catch(error=>{
console.log(error);
});
</script>
</body>
现在下载特定食谱:
<body>
<script>
const getIds = new Promise((resolve, reject)=>{
setTimeout(()=>{
const downloadSuccessfull = true;
const recipeId = [83938, 73838, 7638];
if(downloadSuccessfull){
resolve(recipeId);
}else{
reject('download failed 404');
}
}, 1500);
});
const getRecipe = recID => {
return new Promise((resolve, reject)=>{
setTimeout(id => {
const downloadSuccessfull = true;
if (downloadSuccessfull){
const recipe = {title:'Fresh Apple Juice', publisher:'Suru'};
resolve(`${id}: ${recipe.title}`);
}else{
reject(`${id}: recipe download failed 404`);
}
}, 1500, recID)
})
}
getIds.then(IDs=>{
console.log(IDs);
return getRecipe(IDs[2]);
}).
then(recipe =>{
console.log(recipe);
})
.catch(error=>{
console.log(error);
});
</script>
</body>
现在我们可以像getRecipe一样编写另一个方法调用 allRecipeOfAPublisher ,该方法也会返回一个Promise,我们可以编写另一个then()来接收allRecipeOfAPublisher的可解决Promise,我希望您现在可以这样做自己动手。
因此,我们学习了如何构造和使用Promise,现在让我们使用es8中引入的async / await来更轻松地使用Promise。
<body>
<script>
const getIds = new Promise((resolve, reject)=>{
setTimeout(()=>{
const downloadSuccessfull = true;
const recipeId = [83938, 73838, 7638];
if(downloadSuccessfull){
resolve(recipeId);
}else{
reject('download failed 404');
}
}, 1500);
});
const getRecipe = recID => {
return new Promise((resolve, reject)=>{
setTimeout(id => {
const downloadSuccessfull = true;
if (downloadSuccessfull){
const recipe = {title:'Fresh Apple Juice', publisher:'Suru'};
resolve(`${id}: ${recipe.title}`);
}else{
reject(`${id}: recipe download failed 404`);
}
}, 1500, recID)
})
}
async function getRecipesAw(){
const IDs = await getIds;
console.log(IDs);
const recipe = await getRecipe(IDs[2]);
console.log(recipe);
}
getRecipesAw();
</script>
</body>
在上面的示例中,我们使用了async函数,因为它将在后台运行;在async函数中,我们在每个返回或承诺的方法之前使用 await 关键字,因为要在该位置上等待直到实现了承诺,换句话说就是在以下代码中,直到返回完ID的getIds完成解析或拒绝程序后,该代码才会停止在该行以下执行代码,然后我们再次使用id调用getRecipe()函数,并使用await关键字等待,直到返回数据。因此,这就是我们终于从回调地狱中恢复过来的方式。
async function getRecipesAw(){
const IDs = await getIds;
console.log(IDs);
const recipe = await getRecipe(IDs[2]);
console.log(recipe);
}
要使用await,我们将需要一个异步函数,我们可以返回一个promise,然后使用then来解决promise,而cath来拒绝promise
来自以上示例:
async function getRecipesAw(){
const IDs = await getIds;
const recipe = await getRecipe(IDs[2]);
return recipe;
}
getRecipesAw().then(result=>{
console.log(result);
}).catch(error=>{
console.log(error);
});
答案 5 :(得分:0)
我开始使用在Sodium框架http://sodium.nz/中实现的严格FRP RX的“增强版本”。
典型的代码如下所示(Scala.js):
def render: Unit => VdomElement = { _ =>
<.div(
<.hr,
<.h2("Note Selector"),
<.hr,
<.br,
noteSelectorTable.comp(),
NoteCreatorWidget().createNewNoteButton.comp(),
NoteEditorWidget(selectedNote.updates()).comp(),
<.hr,
<.br
)
}
selectedNote.updates()
是Stream
,如果selectedNode
(这是Cell
)发生更改,则将触发,NodeEditorWidget
然后进行相应的更新。
因此,根据selectedNode
Cell
的内容,当前编辑的Note
将发生变化。
完整的源代码为here
上面的代码段与以下简单的“创建/显示/更新”示例相对应:
此代码还将更新发送到服务器,因此对更新的实体的更改将自动保存到服务器。
使用Stream
和Cell
来处理所有事件。这些是FRP概念。仅当FRP逻辑与外部世界连接时才需要回调,例如用户输入,编辑文本,按下按钮,返回AJAX调用。
使用FRP(由Sodium库实现)以声明的方式显式描述数据流,因此不需要事件处理/回调逻辑来描述数据流。
FRP(是RX的“更严格”版本)是一种描述数据流图的方法,该数据流图可以包含包含状态的节点。事件触发包含节点(称为Cell
)的状态的状态更改。
钠是高阶FRP库,这意味着使用flatMap
/ switch
原语可以在运行时重新排列数据流图。
我建议看一下Sodium book,它详细说明了FRP如何摆脱所有回调,这些回调对于描述与响应某些外部响应来更新应用程序状态有关的数据流逻辑不是必不可少的刺激。
使用FRP,只需保留那些描述与外部世界交互的回调。换句话说,当一个人使用FRP框架(如Sodium)或当一个人使用“ FRP类”框架(如RX)时,以功能/声明的方式描述数据流。
钠也可用于Javascript / Typescript。
答案 6 :(得分:-1)
如果您对回调和地狱回调一无所知,那就没问题了,第一件事就是回调并回调地狱,例如:hell回调就像我们可以将一个类存储在一个类中。如您所见,嵌套在C,C ++语言中。嵌套的意思是一个类位于另一个类中。
答案 7 :(得分:-3)
使用jazz.js https://github.com/Javanile/Jazz.js
它简化如下:
// run sequential task chained jj.script([ // first task function(next) { // at end of this process 'next' point to second task and run it callAsyncProcess1(next); }, // second task function(next) { // at end of this process 'next' point to thirt task and run it callAsyncProcess2(next); }, // thirt task function(next) { // at end of this process 'next' point to (if have) callAsyncProcess3(next); }, ]);