AngularJS: multiple promises resolving erratically

2019-07-22 18:21发布

问题:

I am assigning two collections into my scope in a controller; both come from services wrapping RestAngular and return promises. Each individually returns the collection I want, but when I assign both to $scope one of them never resolves (it's the same one that fails, no matter the order they're in).

If I log to console by calling the offending service again, then lo and behold the original assignment works perfectly.

The services both work pretty much the same:

.factory('Role', ['Restangular', '$q', function(Restangular, $q){
        var _collection = [];
        var _roleService = Restangular.all('roles');

        return {
            getList: function() {
                // return _roleService.getList();
                var listDeferred = $q.defer();
                _roleService.getList()
                .then(function(list) {
                    listDeferred.resolve(list);
                    _collection = list;
                });
                return listDeferred.promise;
            }
        }
    }
])

and the controller is specified as follows (I've left the console.log in, but removing it causes $scope.roles to never resolve):

.controller('ResourceCtrl', ['$scope', 'Resource', 'Role', function($scope, Resource, Role) {
    $scope.roles = Role.getList();
    $scope.resources = Resource.getList();

    console.log(Role.getList());
}])

Everything is done within Angular so I don't think I'm missing an $apply - but unless I've stumbled on a bug there must be something wrong with my code...

EDIT

I found out what's causing the error, but don't have a solution. My Resource service is very similar to the Role one, but because Resources have Roles, I inject Role into Resource and use it to link individual elements together. Once I deleted that line all the scope / promise stuff came back.

Which I guess takes me back to: how do I link services together reliably (i.e. so Resources with role fields can have the corresponding Role objects attached to them at the service level).

Here's my Resource:

.factory('Resource', ['Restangular', '$q', 'Role',
    function(Restangular, $q, Role){
        var _resourceService = RestAngular.all('resources');
        var _roleService = Role;

        var _convertObjectsToUrls = function(item) {
            for (var property in item) {
                if (item.hasOwnProperty(property) && typeof(item[property]) == 'object' && item[property] != null) {
                    item[property] = item[property].url;
                }
            }
            return item;
        }

        var _convertUrlsToObjects = function(item) {
            for (var property in item) {
                if (item.hasOwnProperty(property) && typeof(item[property]) == 'string' && item[property] != '' && property != 'url' && item[property].substr(0,4) == 'http') {
 /* THIS LINE BREAKS IT */                       item[property] = _roleService.getByUrl(item[property]);
                }
            }
            return item;
        }

        var _getIdFromUrl = function(url) {
            var pathElements = url.split('/')
            return pathElements[pathElements.length - 2]
        }

        var _cleanParams = function(item) {
            for (var property in item) {
                if (item.hasOwnProperty(property)) {
                    item[property] = undefined;
                }
            }
            return item;
        }

        return {
            add: function(item) {
                var responseDeferred = $q.defer();
                item = _convertObjectsToUrls(item);

                _resourceService.post(item)
                .then(function(response){
                    response = _convertUrlsToObjects(response);
                    response.name = response.first_name + ' ' + response.last_name;

                    _collection.push(response);
                    responseDeferred.resolve(response);

                    item = _cleanParams(item);
                });
                return responseDeferred.promise;
            },
            edit: function(item) {
                var responseDeferred = $q.defer();
                var idx = _collection.indexOf(item);
                item = _convertObjectsToUrls(item);

                item.customPUT(_getIdFromUrl(item.url))
                .then(function(response){
                    response = _convertUrlsToObjects(response);
                    response.name = response.first_name + ' ' + response.last_name;

                    _collection.splice(idx, 1, response)
                    responseDeferred.resolve(response);
                });
                return responseDeferred.promise;
            },
            delete: function(item) {
                var responseDeferred = $q.defer();
                var idx = _collection.indexOf(item);
                // item = _convertObjectsToUrls(item);

                item.customDELETE(_getIdFromUrl(item.url), {})
                .then(function(response){
                    response = _convertUrlsToObjects(response);

                    _collection.splice(idx, 1)
                    responseDeferred.resolve(response);
                });
                return responseDeferred.promise;
            },
            getList: function() {
                var listDeferred = $q.defer();
                var list = _resourceService.getList()
                .then(function(list) {
                    _.each(list, function(item, index, list){
                        item = _convertUrlsToObjects(item);
                        item.name = item.first_name + ' ' + item.last_name;
                    })
                    listDeferred.resolve(list);
                    _collection = list;
                });
                return listDeferred.promise;
            }
        }
    }
])

回答1:

Not sure from where the Resource service comes and whether is is of importance. Since you return a promise in your Role service, you can just do:

.controller('ResourceCtrl', ['$scope', 'Role', function($scope, Role) {
    Role.getList().then(function(roles){
         $scope.roles = roles;
    });
}])

Alternatively, you can let Angular resolve the role for you (if you are just using the roles in a template, for example)

.controller('ResourceCtrl', ['$scope', 'Role', function($scope, Role) {
      $scope.roles = Role.getList();
 }])

Edit: I also noticed the var _collection = []; which seems not needed. Maybe you can also post the code for the Resource service? This code of your Role service should be sufficient.

.factory('Role', ['Restangular', '$q', function(Restangular, $q){
        return {
            getList: function() {
                var listDeferred = $q.defer();
                Restangular.all('roles').getList().then(function(list) {
                    listDeferred.resolve(list);
                });
                return listDeferred.promise;
            }
        }
    }
])

Even further edit:

This plunkr shows that in angular 1.2rc3 the first method works: http://embed.plnkr.co/SU5UMK7jNffXWnWiV1HE/preview

But if you were to try to take advantage of the automatic unrwapping of promises (which is deprecated in rc3, you are out of luck): http://embed.plnkr.co/zdepkLCesYkj8pXYFTN2/preview