如何在循环调用时显示 JS 异步结果?

How to display a JS asynchronous result when called on a loop?

我正在做一个个人项目,我被困在似乎是 "basic" 同步/异步 Javascript 的问题上。

总而言之,我正在调用异步 API,然后在屏幕上显示结果。
格式化的期望结果是:"At X kilometers from Y",其中 X 是从 API 计算并正确返回的,Y 是地名(未正确显示)。

这是我的代码,为了更好地理解,有一些注释:

//loadedLandmarks.length is **always** between 1 and 3. No exception.
//In this sample, let's say we have 3 items in loadedLandmarks
for(var i = 0 ; i < loadedLandmarks.length ; i++)
{
    var currentLandmarkName = loadedLandmarks[i].customInfo.Name;
    landmarksName.push(loadedLandmarks[i].customInfo.Name);

    //landmarkName contains the good names, for example : "My school", "My home", "My favorite nightclub" (here after 3 pass on the loop)
    console.log(landmarksName);

    //DisplayDistanceFromLandmarks is my method which calls the asynchronous API. It seems OK.
    DisplayDistanceFromLandmarks(pos, i).then(function(response) {
        //The response variable contains correct informations from the API
        var origins = response.originAddresses;
        var destinations = response.destinationAddresses;
        var results = response.rows[0].elements;

        //I explain this line below
        console.log(loadedLandmarks)

        //Then I'm formatting the result to display it on screen (I only paste here the interesting part)
        distances += "<br />At " + results[0].distance.value + "kms from" + currentLandmarkName;

        return distances;
}).done( /*some other things*/ );

显示的结果是:

At 5 kms from [insert here the LAST currentLandmarkName]
At 8.5 kms from [insert here the LAST currentLandmarkName]
At 0.2 kms from [insert here the LAST currentLandmarkName]

而应该是:

At 5 kms from [insert here the FIRST currentLandmarkName]
At 8.5 kms from [insert here the SECOND currentLandmarkName]
At 0.2 kms from [insert here the THIRD currentLandmarkName]

我不明白的是,当我写console.log(loadedLandmarks)时,数组的内容是正确的,loadedLandmarks[0].Name=名字,loadedLandmarks[1].Name=第二个名字等等 但是,i 始终等于 3,而 currentLandmarkName 始终等于最后一个地标名称。
好像被覆盖了,我不明白为什么。


我是 JS 和异步问题的新手,有人可以解释我为什么会遇到这种行为,而且非常重要的是,如何纠正它?

此方法 DisplayDistanceFromLandmarks 是异步的,这意味着它不会阻止其余代码的执行。所以当你像这样在回调中形成 html 字符串 distances 时:

distances += "<br />At " + results[0].distance.value + "kms from" + currentLandmarkName;

for 循环已经结束,回调函数调用已排队,稍后 return 它们的值。回调函数在其范围内没有 currentLandmarkName,因为它定义为

var currentLandmarkName = loadedLandmarks[i].customInfo.Name;

我觉得你可以做两件事:

  1. 将变量 currentLandmarkName 传递给 DisplayDistanceFromLandmarks 方法,从而将其引入作用域。
  2. 您也可以尝试使用 while 循环并增加计数器,即仅在回调内增加变量 i。这样你就强制异步行为是同步的。因此,循环的下一次迭代只会在解决承诺并评估回调后发生。

我看到了一篇非常好的文章,它解释了 javascript 的异步行为和闭包的概念。你可能想读一读 http://www.javascriptissexy.com/understand-javascript-closures-with-ease/

希望它能让您朝着正确的方向开始。干杯!

嗨, 这是 JavaScript 和闭包的典型问题。

在传递给 DisplayDistanceFromLandmarks(pos, i).then() 的函数中,您使用了变量 currentLandmarkName,这给您带来了一些麻烦。

这个变量不是在该函数内部定义的,而是在 for 循环中定义的:

for(var i = 0 ; i < loadedLandmarks.length ; i++)
{
    var currentLandmarkName = loadedLandmarks[i].customInfo.Name;
    ...

对于 for 循环的每次调用,您调用 DisplayDistanceFromLandmarks(pos, i).then() 并在 then 调用中定义一个新的匿名函数。这个函数在创建时有一个与之关联的闭包。在这个闭包中有一个 referencecurrentLandmarkName注意:这里是对变量的引用而不是它的值。感谢这个参考,你可以使用它。

问题是代码是异步的,所以在for循环结束后调用了then函数。当 for 循环结束时,currentLandmarkName 始终具有最后一个 loadedLandmarks 的值:

var currentLandmarkName = loadedLandmarks[i].customInfo.Name;

i 等于 loadedLandmarks.length-1

所以当 then 函数被调用时,它会尝试访问 currentLandmarkName 的引用,它总是等于最后一个元素:

 loadedLandmarks[loadedLandmarks.length-1].customInfo.Name;

因此你总是得到 [insert here the LAST currentLandmarkName]

var i = 0;
var DisplayDistance = function () {
        var currentLandmarkName = loadedLandmarks[i].customInfo.Name;
        landmarksName.push(loadedLandmarks[i].customInfo.Name);

        //landmarkName contains the good names, for example : "My school", "My home", "My favorite nightclub" (here after 3 pass on the loop)
        console.log(landmarksName);

        //DisplayDistanceFromLandmarks is my method which calls the asynchronous API. It seems OK.
        DisplayDistanceFromLandmarks(pos, i).then(function(response) {
            //The response variable contains correct informations from the API
            var origins = response.originAddresses;
            var destinations = response.destinationAddresses;
            var results = response.rows[0].elements;

            //I explain this line below
            console.log(loadedLandmarks)

            //Then I'm formatting the result to display it on screen (I only paste here the interesting part)
            distances += "<br />At " + results[0].distance.value + "kms from" + currentLandmarkName;

            i++;
            if (i < loadedLandmarks.length) {
                DisplayDistance();
            }

            return distances;
    }).done( /*some other things*/ );
}
DisplayDistance();