简体   繁体   中英

JavaScript nested promise scoping and AngularJS

I have a real problem with JavaScript promises that I've been trying to solve for the last few hours and I just can't seem to fix it. My experience with promises is limited so I'm open to the idea that my approach is simply incorrect.

Right now I'm building an app that requires a two-step process:

  1. Connect to an external PaaS service, which returns a promise
  2. Within that promise, retrieve some data

Here's a sample of a factory I created:

app.factory('serviceFactory', [
    function() {
        var getData = function getData() {
            service.connect(apiKey).then(function() {
                    service.getData('dataStore').then(function(result) {
                        // Retrieve data
                        return result;
                    }, errorFunction);
                },
                errorFunction);
        };
        return {
                getData: getData
        };
    }

]);

As you can see, there are nested promises here. What's causing me problems is when I try to use the data from the most deeply-nested promise within an AngularJS view. Specifically, I want to use the data from that promise in an ng-repeat statement. But no matter what I try, it just won't show up. I've attempted to assign data within the promise instead of returning, like so:

service.getData('dataStore').then(function(result) {
    // Retrieve data
    // Assigned the enclosing scope's this to 'self'
    self.data = result;
}, errorFunction);

That doesn't work either. I've tried a variety of other approaches, but I just can't seem to get that data to the view. There's no problem getting it to show up in a console.log(data) call, so I know the data is coming back correctly. Does anyone have experience solving a problem like this?

I would suggest that you'll try to avoid nested promises. You can take a look at this blog post , which will let you see how you can avoid 'promise soup' and have promise chaining instead.

As for your question, I would recommend the following:

A quick solution will be to fix your problem. You are returning the factory method wrong:

app.factory('serviceFactory', [
    function() {
        var getData = function getData() {
            return service.connect(apiKey).then(function() {
                    service.getData('dataStore').then(function(result) {
                        // Retrieve data
                        return result;
                    }, errorFunction);
                },
                errorFunction);
        };//here you should close the 'getData method
        return {
            getData: getData
        };   
    }
]);

But, you can refactor your code to chain your promises. Something like:

app.factory('serviceFactory', [
    function() {
        var connect = function connect() {
            return service.connect(apiKey);
        };
        var getData = function getData(data) {
            return service.getData(data);
        };       
        return {
            getData: getData,
            connect: connect
        };
    }
]);

Now, you can do something like this:

serviceFactory.connect(apiKey)
     .then(serviceFactory.getData)
     .then(function(result){
           //use data here
      })

All of this should be tested - you can add a plunker or jsbin if you want a working solution...

EDIT

I think that you have another problem here. You are mixing between serviceFactory and service . I'm not sure that I understand if this is the same service, or which is who. Can you provide a more detailed code or add plunker/jsbin etc.

I've edited this answer, which I originally deleted because I didn't explain what I meant very clearly and it garnered some downvotes (without explanation, but that's my guess). Anyway, here is a more complete answer.

I suspect that your problem is that whatever PaaS you are using has no awareness of Angular, and Angular likewise has no awareness of the PaaS. You say in your question that the PaaS has methods that return promises, but if Angular is not aware of those promises, then, when the promises resolve, Angular does not know to update the DOM. Angular does this via the digest cycle which is where Angular checks everything that it is watching to see if it has changed. When using $q (or other Angular services like $http ), Angular knows to automatically kick off a digest cycle when they resolve. It does not, however, kick off a digest cycle when promises created by other means resolve.

This is what I think is happening in your code. Your PaaS is giving you promises, which are resolving properly (you said you can see the results via console), but your HTML is not being updated.

I modified the plunkr we were working on to demonstrate this in action. I created a mock PaaS (not knowing what you are using) that creates promises using jQuery and resolves them. As you can see, when the promises resolve, the result is logged to the console, but the DOM is not resolved.

angular.module("app",[])
  .value("mockPaaS", mockPaaS)
  .factory("dataFactory", function($q, mockPaaS){

    function getData(){
      return mockPaaS.connect()
        .then(mockPaaS.getData);
    }
    return {
      getData: getData
    }
  })
  .controller("DataController", function (dataFactory) {
      var vm = this;
      dataFactory.getData().then(function(result){
        console.log(result);
        vm.dataArr = result;
      });
  })
  .directive("myApp", function(){
      return {
        bindToController: true,
        controller: "DataController",
        controllerAs: 'myApp',
        template: "<div ng-repeat='i in myApp.dataArr'>{{i}}</div>"
    };
  });

I was originally suggesting that you could solve this problem by adding a $scope.$apply() after you capture the result of the promise. I've forked the Plunker and you can see here it does, in fact update the DOM.

  .controller("DataController", function ($scope, dataFactory) {
      var vm = this;
      dataFactory.getData().then(function(result){
        console.log(result);
        vm.dataArr = result;
        $scope.$apply();
      });
  })

There is, however, a more idiomatic solution. When you get a promise from outside angular that you need to use in Angular, you can wrap that promise using $q.when (an Angular aware promise), and when the external promise resolves, Angular should kick off it's digest cycle naturally.

  .factory("dataFactory", function($q, mockPaaS){
    function getData(){
      return $q.when(mockPaaS.connect()
        .then(mockPaaS.getData));
    }
    return {
      getData: getData
    }
  })
  .controller("DataController", function (dataFactory) {
      var vm = this;
      dataFactory.getData().then(function(result){
        console.log(result);
        vm.dataArr = result;
      });
  })

Ben Nadel gives a nice explanation of this issue here .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM