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

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

本文关键字:显示 JS 异步 结果 调用 循环      更新时间:2023-09-26

我正在做一个个人项目,我被一个似乎是"基本的"同步/异步Javascript问题卡住了。

总而言之,我调用了一个异步API,然后在屏幕上显示结果。格式化后的期望结果是:"At X km 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*/ );

显示的结果如下:

距离[insert here the LAST currentLandmarkName] 5公里
距离[插入这里的lastcurrentlandmarkname] 8.5公里
距离[insert here the LAST currentLandmarkName] 0.2公里

而它应该是:

距离[insert here the FIRST currentLandmarkName] 5公里
在距[插入此处的SECOND currentLandmarkName] 8.5公里处
在距离[此处插入第三个currentLandmarkName] 0.2公里处

我不明白的是,当我写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循环已经结束,回调函数调用排队,稍后返回它们的值。回调函数的作用域中没有currentLandmarkName,因为它的定义是

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

我觉得有两件事你可以做:

  1. 将变量currentLandmarkName传入DisplayDistanceFromLandmarks方法,从而使其进入作用域。
  2. 你也可以尝试使用while循环并增加计数器,即只在回调中增加变量i。通过这种方式,可以强制异步行为为同步行为。因此,循环的下一次迭代只会发生在promise被解析并且callback被求值之后。

我看到过一篇非常好的文章,解释了javascript的这种异步行为和闭包的概念。你可以给它一个read 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调用中定义一个新的匿名函数。这个函数在创建时,有一个关联的闭包。在这个闭包中有一个指向currentLandmarkName引用注意:这里是对变量的引用,而不是它的值。有了这个参考,你就可以使用它了。

问题是代码是异步的,所以then函数在for循环结束后被调用。当for循环结束时,currentLandmarkName总是最后一次加载的值:

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();