Answers:
这另一个问题有一些Javascript回调地狱的例子: 如何避免Node.js中异步函数的长时间嵌套
Javascript中的问题在于,“冻结”计算并使“其余部分”(异步地)执行的唯一方法是将“其余部分”放入回调中。
例如,假设我要运行如下代码:
x = getData();
y = getMoreData(x);
z = getMoreData(y);
...
如果现在我想使getData函数异步,这意味着我有机会在等待其他代码返回值时运行一些其他代码,会发生什么情况?在Javascript中,唯一的方法是使用延续传递样式重写所有与异步计算有关的内容:
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是回调地狱的女王,是单线程的。
我对RX一点也不了解,但是通常可以通过在编程语言中添加对异步计算的本机支持来解决此问题。实现可能有所不同,包括:异步,生成器,协程和callcc。
在Python中,我们可以使用类似于以下内容的代码来实现该先前的循环示例:
def myLoop():
for i in range(10):
doSomething(i)
yield
myGen = myLoop()
这不是完整的代码,但是其思想是“ yield”暂停我们的for循环,直到有人调用myGen.next()。重要的是,我们仍然可以使用for循环来编写代码,而无需像在递归loop
函数中那样将逻辑“由内而外” 。
解决Rx如何解决回调地狱的问题:
首先,让我们再次描述回调地狱。
假设有一个案例,我们必须使用http来获取三种资源-人,星球和星系。我们的目标是找到人类所居住的星系。首先,我们必须先获得人类,然后是行星,然后是星系。这是三个异步操作的三个回调。
getPerson(person => {
getPlanet(person, (planet) => {
getGalaxy(planet, (galaxy) => {
console.log(galaxy);
});
});
});
每个回调都是嵌套的。每个内部回调都依赖于其父级。这导致回调地狱的“厄运金字塔”风格。该代码看起来像一个>符号。
要在RxJs中解决此问题,您可以执行以下操作:
getPerson()
.map(person => getPlanet(person))
.map(planet => getGalaxy(planet))
.mergeAll()
.subscribe(galaxy => console.log(galaxy));
使用mergeMap
AKA flatMap
运算符,您可以使其更加简洁:
getPerson()
.mergeMap(person => getPlanet(person))
.mergeMap(planet => getGalaxy(planet))
.subscribe(galaxy => console.log(galaxy));
如您所见,代码是扁平化的,并且包含单个方法调用链。我们没有“厄运金字塔”。
因此,避免了回调地狱。
万一您想知道,promise是避免回调地狱的另一种方法,但是promise很渴望,不像可观察对象那样懒惰,并且(通常来说)您不能轻易取消它们。
回调地狱是在异步代码中使用函数回调变得晦涩难懂或难以遵循的任何代码。通常,当间接调用的级别不只一种时,使用回调的代码将变得更难遵循,更难重构和更难测试。代码气味是由于传递函数文字的多层而导致的多个缩进级别。
当行为具有相关性时,即当A必须在B之前必须发生在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是“反应性扩展”的缩写。我没有使用过它,但是Googling建议它是一个基于事件的框架,这很有意义。事件是使代码按顺序执行而不会产生脆弱耦合的常见模式。您可以使C监听事件“ bFinished”,该事件仅在B被称为监听“ aFinished”之后发生。然后,您可以轻松地添加额外的步骤或扩展这种行为,并且仅通过在测试用例中广播事件就可以轻松地测试代码是否按顺序执行。
回调地狱意味着您在另一个回调内部的回调中,并且将转到第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调用成功,您可以打电话的决心,并通过传递数据解析,您可以通过使用得到这个数据则() 。但是,如果您的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>
现在我们可以编写另一个方法调用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函数内部,我们在每个返回或为promise的方法之前使用了await关键字,因为要在该位置上等待直到实现该promise,换句话说,以下代码,直到返回完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 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);
});
可以避免回调地狱的一种方法是使用FRP,它是RX的“增强版本”。
我最近开始使用FRP,因为我找到了一个名为Sodium
(http://sodium.nz/)的良好实现。
一个典型的代码如下所示(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
将会更改。
这段代码几乎完全避免了Callback-s,Cacllback-s被推到了应用程序的“外层” /“表面”,状态处理逻辑与外部世界交互。在内部状态处理逻辑(实现状态机)内传播数据不需要回调。
完整的源代码是 这里
上面的代码段与以下简单的“创建/显示/更新”示例相对应:
此代码还将更新发送到服务器,因此对更新的实体的更改将自动保存到服务器。
所有事件处理都通过使用Stream
s和Cell
s。这些是FRP概念。仅当FRP逻辑与外部世界连接时才需要回调,例如用户输入,编辑文本,按下按钮,返回AJAX调用。
使用FRP(由Sodium库实现)以声明的方式显式地描述了数据流,因此不需要事件处理/回调逻辑来描述数据流。
FRP(是RX的“更严格”版本)是一种描述数据流图的方法,该数据流图可以包含包含状态的节点。事件触发包含节点的状态的变化(称为Cell
s)。
钠是高阶FRP库,这意味着使用flatMap
/ switch
原语可以在运行时重新排列数据流图。
我建议看一看Sodium书,它详细解释了FRP如何摆脱所有回调,这些回调对于描述与响应某些外部刺激来更新应用程序状态有关的数据流逻辑不是必不可少的。
使用FRP,只需保留描述与外部世界交互的那些回调。换句话说,当一个人使用FRP框架(如Sodium)或当一个人使用“类似FRP”的框架(如RX)时,以功能/声明的方式描述数据流。
钠也可用于Javascript / Typescript。
如果您对回调和地狱回调一无所知,那就没问题了,第一件事就是回调并回调地狱,例如:地狱回调就像我们可以将一个类存储在一个类中。关于嵌套在C,C ++语言中的内容。嵌套的意思是一个类位于另一个类中。
使用jazz.js https://github.com/Javanile/Jazz.js
它简化如下:
//运行顺序任务链 jj.script([ //第一项任务 函数(下一个){ //在此过程结束时,“ next”指向第二个任务并运行它 callAsyncProcess1(next); }, //第二项任务 函数(下一个){ //在此过程结束时,“下一步”指向任务并运行 callAsyncProcess2(next); }, //渴任务 函数(下一个){ //在此过程结束时,“ next”指向(如果有) callAsyncProcess3(next); }, ]);