Nester 延迟 ajax 循环调用

Nester deferred ajax calls in loops

我有一个复杂的(至少对我来说)设置嵌套循环、ajax 调用和延迟。该代码正在调用 API,解析出相关数据,然后使用它进一步调用其他 API。

几乎按预期工作。我使用这个问题的答案 (Using $.Deferred() with nested ajax calls in a loop) 来构建它。这是我的代码:

function a() {
var def = $.Deferred();
var req = [];

for (var i = 0 /*...*/) {
    for (var j = 0 /*...*/) {
        (function(i, j) {
            req.push($.ajax({
                //params
            }).done(function(resp) {
                var def2 = $.Deferred();
                var req2 = [];

                for (var k = 0 /*...*/) {
                    for (var l = 0 /*...*/) {
                        req2.push(b(l));
                    }
                }

                $.when.apply($, req2).done(function() {
                    console.log("Got all data pieces");
                    def2.resolve();
                })
            }));
        })(i, j);
    }
}

$.when.apply($, req).done(function() {
    console.log("Got all data");
    def.resolve();
});

return def.promise();

}

function b(j) {
var def = $.Deferred();

$.when.apply(
    $.ajax({
        //params
    })
).then(function() {
    console.log("Got data piece #" + l);
    def.resolve();
});

return def.promise();

}

function main() {
//...

$.when.apply($, a()).then(function() {
    console.log("All done");
    displayPage();
})

//...

}

这是我希望在调用完成后看到的内容

(In no specific order)
Got data piece #1
Got data piece #0
Got data piece #2
Got all data pieces
Got data piece #2
Got data piece #1
Got data piece #0
Got all data pieces
Got data piece #0
Got data piece #1
Got data piece #2
Got all data pieces
Got all data            <-- These two must be last, and in this order
All done

这是我看到的

All done
Got data piece #0
Got data piece #1
Got data piece #2
Got all data pieces
Got data piece #0
Got data piece #1
Got data piece #2
Got all data pieces
Got data piece #0
Got data piece #1
Got data piece #2
Got all data pieces

我在调试器中单步执行它,函数 a() 中的 'Got all data' 行在其他一切完成后以正确的顺序打印,之后应该调用 def.resolve() 并且解决返回的承诺。

然而,在 main() 中,a() 被视为立即解析,代码直接跳转到打印 'All done' 并显示页面。关于为什么它没有按预期等待有什么想法吗?

您说明了一组代码并说它没有按照您的预期进行,但您还没有真正描述整个问题。所以,我实际上并不知道推荐什么代码。我们在这里处理实际问题比伪代码问题要好得多。所以,相反,我能做的是概述你的代码中的一堆错误:

期望并行异步操作的串行顺序

根据您所说的预期,您似乎缺少控制异步操作​​的基本逻辑。当您对一系列已经开始的承诺使用 $.when() 时,您正在 运行 并行处理一大堆异步操作。他们的完成顺序是完全不可预测的。

是的,您似乎希望能够并行 运行 一大堆 b(i) 并按顺序完成它们。情况似乎是这样,因为您说您期待这种类型的输出:

获得数据块 #0 得到数据块 #1 得到数据块 #2

其中每个语句都是通过完成某些 b(i) 操作生成的。

这根本不会发生(或者如果在现实世界中发生那将是盲目的运气,因为没有保证顺序的代码)。现在,您可以并行 运行 它们并使用 $.when() 来跟踪它们,并且 $.when() 会在它们全部完成时通知您并按顺序收集所有结果。但是当该组中的每个单独的异步操作完成时都是偶然的。

因此,如果您真的希望每个 b(i) 操作都 运行 并按顺序完成,那么您必须有目的地对它们进行排序(运行 一个,等待它完成,然后 运行 下一个,等等...)。一般来说,如果一个操作不依赖于另一个,最好并行 运行 它们并让 $.when() 跟踪它们并为您排序结果(因为您通常可以更快地获得最终结果通过 运行 将它们全部并行而不是排序)。

在很多地方创建不必要的延迟 - promse 反模式

在此代码中,根本不需要创建延迟。 $.ajax() 已经 return 是一个承诺。你可以只使用那个承诺。所以,而不是这个:

function b(j) {
    var def = $.Deferred();

    $.when.apply(
        $.ajax({
            //params
        })
    ).then(function() {
        console.log("Got data piece #" + l);
        def.resolve();
    });
    return def.promise();
}

你可以这样做:

function b(j) {
    return $.ajax({
        //params
    }).then(function(data) {
        console.log("Got data piece #" + l);
        return data;
    });
}

请注意,您只是直接 return $.ajax() 已经生成的承诺,根本不需要创建延迟。这对于错误处理也更加可靠。您的方法被称为反模式的原因之一是您根本不处理错误(使用此反模式时的常见错误)。但是,改进后的代码会像应该的那样将错误传播回调用者。在您的版本中,如果 $.ajax() 调用拒绝了它的承诺(由于错误),您的延迟将永远不会解决,调用者也永远不会看到错误。现在,您可以编写额外的代码来处理错误,但没有理由这样做。只是 return 你已经拥有的承诺。当使用 return 承诺的异步操作进行编码时,您几乎不需要创建自己的延迟。

$.when() 只有当你有多个 promise 时才需要

在你的b()函数中,这段代码中不需要使用$.when()

$.when(
    $.ajax({
        //params
    })).then(...);

当你有一个单一的承诺时,你直接在上面使用 .then()

    $.ajax({
        //params
    }).then(...);

仅当您有多个承诺并且想知道所有承诺何时完成时才使用 $.when()。如果您只有一个承诺,只需使用它自己的 .then() 处理程序。

更多反模式 - 仅 return 来自 .then() 处理程序的承诺

在你的内部循环中,你有这个:

            $.when.apply($, req2).done(function() {
                console.log("Got all data pieces");
                def2.resolve();
            })

这里有几处错误。目前尚不清楚您要做什么,因为 def2 是一个没有其他用途的延迟。因此,看起来您正试图在这 req2 组承诺完成时告诉某人,但没有人使用它。此外,它是反模式的另一个版本。 $.when() 已经 return 是一个承诺。当 $.when() 完成时,您不需要创建延迟解析。您可以只使用 $.when() 已经 returns.

的承诺

虽然我不完全了解您的意图,但您似乎应该做的是完全摆脱 def2 延迟并执行此操作:

            return $.when.apply($, req2).done(function() {
                console.log("Got all data pieces");
            });

从它所在的 .then() 处理程序返回此承诺会将此操作序列链接到父级承诺,并使父级承诺等待解决此新承诺(这与所有时间相关) req2 承诺完成)在父承诺解决之前。这就是您如何使父承诺依赖于 .then() 处理程序中的其他承诺。您 return 来自 .then() 处理程序的承诺。

而且,完全相同的问题也适用于您的外部 $.when.apply($, req)。您根本不需要 deferred 。只需使用 $.when() 已经 return 的承诺。

放在一起

这是您的代码的清理版本,它在多个地方去除了反模式。这不会改变 b(i) 调用之间的顺序。如果您关心这一点,这是一个更大的变化,我们需要看到更多 real/actual 问题才能知道最好推荐什么。

function a() {
    var req = [];

    for (var i = 0 /*...*/) {
        for (var j = 0 /*...*/) {
            (function(i, j) {
                req.push($.ajax({
                    //params
                }).then(function(resp) {
                    var req2 = [];

                    for (var k = 0 /*...*/) {
                        for (var l = 0 /*...*/) {
                            req2.push(b(l));
                        }
                    }

                    return $.when.apply($, req2).done(function() {
                        console.log("Got all data pieces");
                    });
                }));
            })(i, j);
        }
    }

    return $.when.apply($, req).done(function() {
        console.log("Got all data");
    });
}    

function b(j) {
    return $.ajax({
            //params
    }).then(function(data) {
        console.log("Got data piece #" + l);
        return data;
    });
}

function main() {
    //...

    a().then(function() {
        console.log("All done");
        displayPage();
    });

    //...
}

P.S。如果你想按顺序处理同一组中的 b(i) 结果,那么不要对单个承诺使用 .then() 处理程序,因为它们将以任意顺序执行。相反,使用 $.when().then(result1, result2, ...) 附带的结果并在那里处理它们。尽管各个 promise 以任意顺序完成,但 $.when() 会将结果收集到原始顺序中,因此如果您在 $.when() 处理程序中处理结果,那么您可以按顺序处理它们。