Nested promises not resolving until a button is cl

2019-04-15 23:26发布

问题:

I'm building an AngularJS app (using 1.2.0 RC1) that uses a REST API for saving data that the user has entered. The user will enter data about a company and it consists of two core components that I need to save:

  • The company details
  • Contacts within that company

I have 1 REST method to call to save the company and then I have another that I call for each contact to be saved.

I planned to use $q to track all the requests and resolve them once it's completed.

The problem I've hit is that although the REST API is being called successfully and all my data is being saved my controller isn't being notified when all the promises have resolved until I click a button on the screen, such as the add contact button. I've tried eliminating the REST API as a source of problems but it still happens when simulating delayed jobs using setTimeout.

Here's my service module:

angular.module('myApp', ['restService'])
    .factory('service', ['client', '$q', function (client, $q) {
        return {
            create: function (name, people) {
                var deferred = $q.defer();

                setTimeout(function () {
                    console.log('saved company ' + name);

                    var promises = people.map(function (person) {
                        var dfd = $q.defer();

                        setTimeout(function () {
                            console.dir(person);
                            console.log('Person saved');
                            dfd.resolve();
                        }, 100);

                        return dfd.promise;
                    });

                    $q.all(promises).then(deferred.resolve);
                }, 100);

                return deferred.promise;
            }
        };
    }]);

And here's my controller:

angular.module('club', ['myApp'])
    .controller('RegisterCtrl', ['$scope', 'service', function ($scope, service) {
        $scope.addPerson = function () {
            $scope.company.people.push({
                firstName: '',
                lastName: '',
                email: ''
            });
        };

        $scope.removePerson = function (person, index) {
            $scope.company.people.splice(index, 1);
        };

        $scope.save = function (company) {
            service.createClub(company.name, company.people).then(function () {
                console.log('And we\'re done');
            });
        };

        $scope.company = {
            name: '',
            people: []
        };
    }]);

So when I invoke the save method (which is ng-click bound to a button) I will have a console.log set like this:

saved company
Object
Person saved
Object
Person saved

Notice there's no And we're done output. As soon as I click the button that addPerson is ng-click bound to I get the final message output.

回答1:

Angular doesn't magically watch all timeouts. It needs a hook to be told that something might have changed, which triggers a $digest loop.

You can do it manually, by calling $scope.$apply(), or by using inbuilt handlers (like ng-click). But for this use case there's also the $timeout service which you can use in place of setTimeout.

See API Reference/ng/$timeout.



回答2:

It is highly recommended to use the AngularJS $http service to communicate with your REST API since it takes care of updating the model for you.

It returns a promise that you can chain success and error handlers to, so it's very versatile.

Translated to your code, you could have a service like this:

angular.module('myApp', [])
    .factory('person', ['$http', function ($http) {
        return {
            create: function (data) {

                // Return promise
                return $http.post('/yourApiUrl', data);
            }
        };
    }]);

and a controller like this:

angular.module('myApp', [])
    .controller('yourController', ['person', function (person) {

        $scope.company = {
            name: '',
            people: []
        };

        $scope.addPerson = function(data){

            //Save person to API
            person.create(data)

                // Success handler
                .success(function(data, status, headers, config) {

                    // Parse person from response (e.g. if your API 
                    // returns the created object after a POST)
                    var person = data;

                    // Add person to company
                    $scope.company.people.push(person)
                })

                // Error handler
                .error(function(data, status, headers, config) {

                    // Handle error
                });

        };

    }]);

By returning the promise from your service, you can customize the success and error handlers in your controller, allowing very flexible solutions.

And you don't have to create your own promises. AngularJS will handle it all for you.

I hope that helps!



回答3:

You always want to call deferred.resolve() and deferred.reject() from within a scope context. In your case, you are calling code from outside the angular framework (setTimeout, and in real use your JS client API for Azure mobile services as you mentioned in a comment), so you should wrap dfd.resolve() in a $rootScope.$apply() (and don't forget to inject $rootScope into your service).

http://plnkr.co/edit/3sCHEjrtMdqCvFQ7ILTD

This Plunkr shows a working example. If you change the $rootScope.$apply(dfd.resolve()) back to just dfd.resolve(), it has the problem you described in your question.

Note that is is probably also better to use $rootScope.$apply(function(){dfd.resolve()}) instead, for the reasons listed in this article:

http://jimhoskins.com/2012/12/17/angularjs-and-apply.html