AngularJS promises уведомлять не работает

У меня есть следующий код контроллера:

 .controller('Controller1', function ($scope, MyService) {

    var promise = MyService.getData();
    promise.then(function(success) {
        console.log("success");
    }, function(error) {
        console.log("error");
    }, function(update) {
        console.log("got an update!");
    }) ;

}

И в моих services.js:

 .factory('MyService', function ($resource, API_END_POINT, localStorageService, $q) {
   return {
       getData: function() {
           var resource = $resource(API_END_POINT + '/data', {
               query: { method: 'GET', isArray: true }
           });

           var deferred = $q.defer();
           var response = localStorageService.get("data");
           console.log("from local storage: "+JSON.stringify(response));
           deferred.notify(response);

           resource.query(function (success) {
               console.log("success querying RESTful resource")
               localStorageService.add("data", success);
               deferred.resolve(success);
           }, function(error) {
               console.log("error occurred");
               deferred.reject(response);
           });

           return deferred.promise;
       }
   }

})

Но по какой-то причине вызов deferred.notify никогда не выполняется и не принимается в контроллере. Разве я не здесь что-то не так? Я не уверен, как получить уведомление для выполнения.

Ответ 1

Мне удалось заставить его работать, обнуляя уведомление в функции $timeout:

$timeout(function() {
  deferred.notify('In progress')}
, 0)

Похоже, вы не можете сообщить об этом, прежде чем возвращать объект обещания, что имеет смысл.

Ответ 2

Я попытался воспроизвести вашу проблему здесь. Кажется, что вы не можете называть notify по обещанию напрямую, но должны быть заключены в вызов $apply.

См. также документацию для $q здесь.

Чтобы процитировать точные строки из примера:

так как этот fn выполняет async в следующем повороте цикла событий, нам нужно обернуть наш код в $apply call, чтобы изменения модели были правильно соблюдены.

Вы можете попробовать это самостоятельно и немного изменить свой код:

deferred.notify(response); // should not work

resource.query(function (success) {
    deferred.notify('Returning from resource'); // should work
    console.log("success querying RESTful resource")
    localStorageService.add("data", success);
    deferred.resolve(success);
}, function(error) {
    deferred.notify('caught error!'); //should also work
    console.log("error occurred");
    deferred.reject(response);
});

Ответ 3

источник: http://www.bennadel.com/blog/2800-forcing-q-notify-to-execute-with-a-no-op-in-angularjs.htm

Принуждение $q.notify() Выполнять

Красота события .notify() заключается в том, что наш слой доступа к данным может использовать его, чтобы обслуживать "сразу доступные, но устаревшие" данные, все еще используя событие .resolve() ни для чего, кроме живых данных. Это дает контекст вызова - ваш контроллер - отличное понимание и контроль над тем, какой набор данных кэширован, и независимо от того, хочет ли он [контроллер] включить кэшированные данные.

Но, мы сталкиваемся с немного расы. Служба доступа к данным, которая владеет кэшированными данными, должна вызывать .notify(), прежде чем она вернет обещание вызывающему контексту. Это означает, что ваш контроллер связывается с событием уведомления после вызова .notify(). С философской точки зрения, это должно быть хорошо - Promises (и почти все, что управляется событиями) предназначены для асинхронного вызова привязок, чтобы создать единообразие доступа.

С практической точки зрения, однако, это не совсем так просто. Хотя AngularJS следует этой философии, она также добавляет несколько оптимизаций для сокращения обработки. В нашем случае конкретно, AngularJS не будет планировать обработку обратного вызова в отложенном объекте, если не увидит, что по крайней мере один обратный вызов связан (в противном случае он думает, что мир не слушает). Таким образом, наш контроллер никогда не будет уведомлен о кэшированных данных.

Чтобы обойти это, мы можем связать наш сервисный уровень с функцией not-op (без операции) для уведомления, прежде чем он вызовет .notify(). Таким образом, когда он вызывает call.notify(), AngularJS увидит, что зарегистрирован хотя бы один обратный вызов, и он запланировал очистку ожидающей очереди в следующем тике (который реализуется через $rootScope. $EvalAsync()). Это позволяет нашему контроллеру получать уведомления о кэшированных данных, даже если он связывается с событием уведомления после вызова .notify().

Чтобы увидеть это в действии, я создал службу friendService, которая возвращает данные с помощью двух разных методов. Оба метода пытаются вернуть кэшированные данные через .notify(), а затем "жить" с помощью .resolve(). Единственное различие между этими двумя методами заключается в том, что один привязывает no-op к уведомляющему событию перед вызовом .notify()

<!doctype html>
<html ng-app="Demo">
<head>
<meta charset="utf-8" />

<title>
    Forcing $q .notify() To Execute With A No-Op In AngularJS
</title>

<link rel="stylesheet" type="text/css" href="./demo.css"></link>
</head>
<body ng-controller="AppController">

<h1>
    Forcing $q .notify() To Execute With A No-Op In AngularJS
</h1>

<h2>
    Friends
</h2>

<div ng-switch="isLoading">

    <!-- Show while friends are being loaded. -->
    <p ng-switch-when="true">
        <em>Loading...</em>
    </p>

    <!-- Show once the friends have loaded and are available in the view-model. -->
    <ul ng-switch-when="false">
        <li ng-repeat="friend in friends track by friend.id">
            {{ friend.name }}
        </li>
    </ul>

</div>

<p>
    <a ng-click="load()">Load</a>
    &nbsp;|&nbsp;
    <a ng-click="loadWithNoop()">Load With No-Op</a>
</p>


<!-- Load scripts. -->
<script type="text/javascript" src="../../vendor/angularjs/angular-1.3.13.min.js"></script>
<script type="text/javascript">

    // Create an application module for our demo.
    var app = angular.module( "Demo", [] );


    // -------------------------------------------------- //
    // -------------------------------------------------- //


    // I control the root of the application.
    app.controller(
        "AppController",
        function( $scope, friendService ) {

            $scope.isLoading = false;

            $scope.friends = [];

            // Load the friend data (defaults to "get" method vs. "getWithNoop").
            loadRemoteData();


            // ---
            // PUBLIC METHODS.
            // ---


            // I reload the list of friends using friendService.get().
            $scope.load = function() {

                loadRemoteData( "get" );

            };


            // I reload the list of friends using friendService.getWithNoop().
            $scope.loadWithNoop = function() {

                loadRemoteData( "getWithNoop" );

            };


            // ---
            // PRIVATE METHODS.
            // ---


            // I load the friends from the friend repository. I am passing-in the
            // method name to demonstrate that, from the Controller point-of-view,
            // nothing here is different other than the name of the method. The real
            // substantive difference exists in the implementation of the friend-
            // Service method and how it interacts with $q / Deferred.
            function loadRemoteData( loadingMethod ) {

                console.info( "Loading friends with [", loadingMethod, "]" );

                // Indicate that we are in the loading phase.
                $scope.isLoading = true;

                // When we make the request, we expect the service to try to use
                // cached-data, which it will make available via the "notify" event
                // handler on the promise. As such, we're going to wire up the same
                // event handler to both the "resolve" and the "notify" callbacks.
                friendService[ loadingMethod || "get" ]
                    .call( friendService )
                    .then(
                        handleResolve, // Resolve.
                        null,
                        handleResolve // Notify.
                    )
                ;

                function handleResolve( friends ) {

                    // Indicate that the data is no longer being loaded.
                    $scope.isLoading = false;

                    $scope.friends = friends;

                    console.log( "Friends loaded successfully at", ( new Date() ).getTime() );

                }

            }

        }
    );


    // -------------------------------------------------- //
    // -------------------------------------------------- //


    // I provide access to the friend repository.
    app.factory(
        "friendService",
        function( $q, $timeout ) {

            // Our friend "repository".
            var friends = [
                {
                    id: 1,
                    name: "Tricia"
                },
                {
                    id: 2,
                    name: "Heather"
                },
                {
                    id: 3,
                    name: "Kim"
                }
            ];

            // Return the public API.
            return({
                get: get,
                getWithNoop: getWithNoop
            });


            // ---
            // PUBLIC METHODS.
            // ---


            // I return the list of friends. If the friends are cached locally, the
            // cached collection will be exposed via the promise' .notify() event.
            function get() {

                var deferred = $q.defer();

                // Notify the calling context with the cached data.
                deferred.notify( angular.copy( friends ) );

                $timeout(
                    function networkLatency() {

                        deferred.resolve( angular.copy( friends ) );

                    },
                    1000,
                    false // No need to trigger digest - $q will do that already.
                );

                return( deferred.promise );

            }


            // I return the list of friends. If the friends are cached locally, the
            // cached collection will be exposed via the promise' .notify() event.
            function getWithNoop() {

                var deferred = $q.defer();

                // -- BEGIN: Hack. ----------------------------------------------- //
                // CAUTION: This is a work-around for an optimization in the way
                // AngularJS implemented $q. When we go to invoke .notify(),
                // AngularJS will ignore the event if there are no pending callbacks
                // for the event. Since our calling context can't bind to .notify()
                // until after we invoke .notify() here (and return the promise),
                // AngularJS will ignore it. However, if we bind a No-Op (no
                // operation) function to the .notify() event, AngularJS will
                // schedule a flushing of the deferred queue in the "next tick,"
                // which will give the calling context time to bind to .notify().
                deferred.promise.then( null, null, angular.noop );
                // -- END: Hack. ------------------------------------------------- //

                // Notify the calling context with the cached data.
                deferred.notify( angular.copy( friends ) );

                $timeout(
                    function networkLatency() {

                        deferred.resolve( angular.copy( friends ) );

                    },
                    1000,
                    false // No need to trigger digest - $q will do that already.
                );

                return( deferred.promise );

            }

        }
    );

</script>

</body>
</html>

Как вы можете видеть, контроллер связывает тот же обработчик с "решением" и "уведомляет" событие о обещании. Таким образом, он может обрабатывать кэшированные данные и данные в реальном времени. Единственное различие заключается в том, какой метод сервисного уровня он вызывает - get() и getWithNoop(). И если мы несколько раз вызываем .get() несколько раз, а затем .getWithNoop() несколько раз, мы можем видеть разницу в консоли.