如何在 AngularJS 中异步填充 $scope 变量?

How to asynchronously populate a $scope variable in AngularJS?

我有以下服务:

app.service('Library', ['$http', function($http) {

    this.fonts = [];
    this.families = [];

    // ... some common CRUD functions here ...

    // Returns the font list
    this.getFonts = function() {
        if(_.isEmpty(this.fonts)) this.updateFonts();
        return this.fonts;
    };

    // Returns the family list
    this.getFamilies = function() {
        if(_.isEmpty(this.families)) this.updateFamilies();
        return this.families;
    };

    // Update the font list
    this.updateFonts = function() {
        var self = this;
        $http.get(BACKEND_URL+'/fonts').success(function(data) {
            self.fonts = data;
            console.log('Library:: fonts updated', self.fonts);
        });
    };

    // Update the family
    this.updateFamilies = function() {
        var self = this;
        $http.get(BACKEND_URL+'/families').success(function(data) {
            var sorted = _.sortBy(data, function(item) { return item });
            self.families = sorted;
            console.log('Library:: families updated', self.families);
        });
    };
}]);

以及以下主控制器代码:

app.controller('MainController', ['$scope', '$state', 'Cart', 'Library', function($scope, $state, Cart, Library) {
    console.log('-> MainController');

    // Serve the right font list depending on the page
    $scope.fonts = $state.is('home.cart') ? Cart.getFonts() : Library.getFonts();
    $scope.families = Library.getFamilies();

}]);

问题是,当视图请求 $scope.fonts 的内容时,它仍然是空的。

加载结束后如何更新$scope.fonts$scope.families

我可以使用 $scope.$watch,但我确信有一种更简洁的方法...

考虑传递回调函数。

服务:

this.getFonts = function(callback) {
    if(_.isEmpty(this.fonts)) this.updateFonts(callback);
    return this.fonts;
};

this.updateFonts = function(callback) {
    var self = this;
    $http.get(BACKEND_URL+'/fonts').success(function(data) {
        self.fonts = data;
        console.log('Library:: fonts updated', self.fonts);
        callback(data);
    });
};

控制器:

Library.getFonts(function (data) { $scope.fonts = data; });

这可以稍微整理一下,因为回调消除了对某些代码的需要,但它会作为示例。

在您的 this.getFonts 函数(和您的其他函数)中,您从 this 调用数据,它指向函数而不是您想要的控制器范围。请尝试以下操作:

var self = this;

self.fonts = [];
self.families = [];

// ... some common CRUD functions here ...

// Returns the font list
self.getFonts = function() {
    if(_.isEmpty(self.fonts)) self.updateFonts();
    return self.fonts; // <-- self.fonts will point to the fonts you want
};

我会尝试将您正在调用的 getScope 和 getFonts 主体包装在

$scope.$apply(function(){ ...body here... });

确保在 self = this 之外声明任何函数。

将调用分配给要存储数据的值,然后return它。

var self = this;
self.data = [];

this.updateFonts = function() {

    self.fonts = $http.get(BACKEND_URL+'/fonts').success(function(data) {
        return data.data
    });
    return self.fonts
};

由于您使用的是 ui-router(我看到了 $state)。您可以在您所在的州使用决议和 return 承诺。

文档:https://github.com/angular-ui/ui-router/wiki

例子:

$stateProvider.state('myState', {
      resolve:{
         // Example using function with returned promise.
         // This is the typical use case of resolve.
         // You need to inject any services that you are
         // using, e.g. $http in this example
         promiseObj:  function($http){
            // $http returns a promise for the url data
            return $http({method: 'GET', url: '/someUrl'});
         }
      },   
       controller: function($scope,promiseObj){
          // You can be sure that promiseObj is ready to use!
          $scope.items = promiseObj.data;
      }
}

在您的情况下,您需要将 this.getFonts 和 getFamilies 变成承诺

this.getFonts = function(){
  return $http.get(BACKEND_URL+'/fonts').success(function(data) {
            self.fonts = data;
            console.log('Library:: fonts updated', self.fonts);
        });
}

有很多方法可以做到这一点,但我认为解决方法是最好的。

这确实是 promises 的目的。您的服务应该 return 一个承诺即被解决。您还可以简化您的服务:

app.service('Library', ['$http', '$q', function($http, $q) {
    var self = this;

    self.families = [];

    // Returns the family list
    self.getFamilies = function() {
        var deferred = $q.defer();

        if(_.isEmpty(self.families)) {
            $http.get(BACKEND_URL+'/families').success(function(data) {
                var sorted = _.sortBy(data, function(item) { return item });
                self.families = sorted;
                deferred.resolve(self.families);
                console.log('Library:: families updated', self.families);
            });
        } else {
            deferred.resolve(self.families);
        }

        return deferred.promise;
    };
}]);

然后在你的控制器中,使用 promises then 方法:

app.controller('MainController', ['$scope', '$state', 'Cart', 'Library', function($scope, $state, Cart, Library) {
    console.log('-> MainController');

    // Serve the right font list depending on the page
    $scope.fonts = $state.is('home.cart') ? Cart.getFonts() : Library.getFonts();
    Library.getFamilies().then(function(result) {
        $scope.families = result;
    });
}]);

由于 $http 的原因,这还没有经过测试,但这里有一个使用 $timeout 的演示:

JSFiddle

谢谢大家的回答!我最终混合使用了回调和承诺,如下所示:

app.service('Library', function($http) {

    // Returns the font list
    this.getFonts = function(callback) {
        if(_.isEmpty(self.fonts)) return self.updateFonts(callback);
        else return callback(self.fonts);
    };

    // Update the font list
    this.updateFonts = function(callback) {
        return $http.get(BACKEND_URL+'/fonts').success(function(data) {
            self.fonts = data;
            callback(data);
        });
    };
});

并且,在控制器中:

app.controller('MainController', function(Library) {
    Library.getFonts(function(fonts) { $scope.fonts = fonts });
});

我尝试了你的所有建议,但这是与我的其余代码一起使用的最佳建议。