推入observableArray后,可观察值消失

时间:2015-01-12 00:47:43

标签: ajax asynchronous knockout.js closures

我从服务器抓取数据并将它们推送到一个可观察的数组中。

我将观察者推入可观察的阵列。

当我将数据推送到observable时,observable包含数据。

然而,只要我将observable推入可观察数组,一些可观察数据就会丢失数据。

      self.mealFoods([]);

      $.ajax({
        url: "/mealsurl/1",
        async: false,
        dataType: 'json',
        success: function(datad) {

          for(var lia = 0; lia < datad.length; lia++){
            var cats_url = "/catsurl/" + datad[lia].category_id;

            var units_by_food_url = "/unitsurl/" + datad[lia].ndb_no;

            var foodThing = new NewFood();

            foodThing.foodId(parseInt(datad[lia].id)); //works

            foodThing.category(parseInt(datad[lia].category_id)); //works

            $.ajax({
              url: cats_url,
              dataType: 'json',
              success: function(dat) {
                foodThing.category_foods(dat); //works
              }
            });

            foodThing.food(datad[lia].ndb_no); //works

            $.ajax({
              url: units_by_food_url,
              dataType: 'json',
              success: function(dat) {
                foodThing.food.units(dat); //works
              }
            });

            foodThing.unit(parseInt(datad[lia].seq)); //works

            foodThing.number_of_unit(datad[lia].this_much); //works



            self.mealFoods.push(foodThing); 

            // At this point when looking inside the mealFoods array: self.mealFoods()[0].food(), self.mealFoods()[0].unit(), self.mealFoods()[0].food.units(), self.mealFoods()[0].category_Foods() ALL ARE EMPTY

          }              
        }
      });

2 个答案:

答案 0 :(得分:5)

你,先生,有一个典型的 async-brain-melt 案例。这是初学者常见的sympton,但从不担心恢复率接近100%。 :)

我打赌你的经验是使用同步语言,也就是说,如果一行写在另一行之后,之前写的行总是在之前执行。

普通的JavaScript函数是同步的。例如:

console.log(1);
console.log(2);

正如所料,这会打印1,然后打印2

但是,异步代码不一定按声明的顺序执行。使用 setTimeout 函数考虑此示例,该函数调度函数以供稍后执行:

setTimeout(function(){ console.log(1); }, 1000);
console.log(2);

现在,输出将是21,因为1仅在setTimeout调用后运行了1000毫秒。

所以,我想你已经开始明白这是如何适用于你的问题的。

您对cats_urlunits_by_food_url的来电是异步。因此,以下代码不会等待让它们完成。因此,当您访问self.mealFoods()[0].food.units()时,success函数尚未获取数据!

您需要做的是正确协调您的异步调用。有很多方法可以实现这一目标。首先,我将教你最简单的策略,只使用函数:

  • 从服务器中获取列表
  • 当你有列表时,迭代每顿饭并开始两个ajax通话(到目前为止,你已经做好了一切)
  • 现在出现了魔力:当你有 ajax调用的结果时,你会调用&#34; itemComplete&#34;功能。此功能将同步两个呼叫 - 只有在两个呼叫完成时才会继续。
  • 最后,拨打&#34; listComplete&#34;每次完成任何项目时都会运行。此功能还必须在继续之前检查所有项目是否完整。

所以,它看起来像这样:

$.ajax({
  url: "/meals/1",
  dataType: 'json',
  success: function(list) {

    var observableArray = ko.observableArray([]); // this will hold your list
    var length = list.length;
    var tries = 0;
    var listComplete = function () {
      tries++;
      if (tries == length) {
        // Hooray! 
        // All your items are complete.
        console.log(observableArray());
      }
    };

    list.forEach(function(item){
      var propertyOneUrl = item.propertyOneUrl;
      var propertyTwoUrl = item.propertyTwoUrl; 

      var propertyOneComplete = false;
      var propertyTwoComplete = false;

      var food = new Food(item.id);

      var itemComplete = function () {
        if (propertyOneComplete && propertyTwoComplete) {
          // This item is complete.
          observableArray.push(food);

          // Let's warn list complete so it can count us in.
          listComplete();
        }
      };

      // Start your ajax calls
      $.ajax({
        url: propertyOneUrl,
        dataType: 'json',
        success: function (propertyOne) {
          food.propertyOne(propertyOne);
          // Declare that your first property is ready
          propertyOneComplete = true;
          // We can't know which property finishes first, so we must call this in both
          itemComplete();
        }
      });

      $.ajax({
        url: propertyTwoUrl,
        dataType: 'json',
        success: function (propertyTwo) {
          food.propertyTwo(propertyTwo);
          // Declare that your second property is ready
          propertyTwoComplete = true;
          // We can't know which property finishes first, so we must call this in both
          itemComplete();
        }
      });
    }); //for each
  } // success   
});

现在,你可能已经意识到这种模式是多么令人厌烦。这就是为什么还有其他方法可以更好地解决这个问题。其中之一是一种名为&#34; Promises&#34;的模式。您可以在以下链接中了解有关它们的更多信息:

https://www.promisejs.org/
http://blog.gadr.me/promises-are-not-optional/

您会很高兴知道jQuery.ajax()会返回一个Promise!所以,现在你可以尝试使用Promises来解决这个问题。您将获得更清晰的代码。

希望你成功!

答案 1 :(得分:2)

这是因为你在循环中进行异步ajax调用。因为无论何时进行ajax调用,循环都会继续,这意味着当响应返回时,分配给foodThing的对象现在不再是在ajax调用之前设置的对象。因为for循环非常快,所以很可能只更新循环中创建的最后一个对象。

如果您看一下这个简单的循环,它会遇到同样的问题:

for (var i = 0; i < 10; i++){
    var a = new NewFood(i);
    $.ajax({
        url: "/catsurl/1",
        dataType: 'json',
        success: function(dat) {
            console.debug(a.id);
        }
    });
}

当ajax调用回来时,a已发生变化,最终发生的事情只有9次被写出10次:http://jsfiddle.net/r6rwbtb9/

为了解决这个问题,我们将使用一个闭包,它基本上将ajax调用包装在一个函数中,在该函数中我们自己包含我们想要做的事情:

for (var i = 0; i < 10; i++){
    var a = new NewFood(i);
    (function (a) {
        $.ajax({
            url: "/catsurl/1",
            dataType: 'json',
            success: function(dat) {
                console.debug(a.id);
            }
        });
    })(a);
}

然后您可以看到数字0-9输出到控制台:http://jsfiddle.net/r6rwbtb9/1/。值得注意的是,您无法确保每个请求都必须以相同的顺序返回。这就是为什么有时数字会以不同的顺序返回0-9,因为有些请求比其他请求更快。

回到你的代码。为了确保您为每个回调更新正确的项目,您需要为每个ajax调用使用闭包。 foodThing.food.units(dat)也存在问题,foodThing.food().units(dat)因为foodThing.food()是可观察的。

因此要封装闭包,我们需要将两个ajax调用更改为:

(function(category_foods){
    $.ajax({
      url: cats_url,
      dataType: 'json',
      success: function(dat) {
        category_foods(dat); 
      }
    });
})(foodThing.category_foods);

(function(units){
    $.ajax({
      url: units_by_food_url,
      dataType: 'json',
      success: function(dat) {
        units(dat); 
      }
    });
})(foodThing.food().units);