AngularJS:在哪里使用 promises?

141

我看到一些使用promises来访问FB Graph API的Facebook登录服务示例。

示例1:

this.api = function(item) {
  var deferred = $q.defer();
  if (item) {
    facebook.FB.api('/' + item, function (result) {
      $rootScope.$apply(function () {
        if (angular.isUndefined(result.error)) {
          deferred.resolve(result);
        } else {
          deferred.reject(result.error);
        }
      });
    });
  }
  return deferred.promise;
}

当服务响应时使用"$scope.$digest() // 手动作用域评估"的方式

示例 #2:

angular.module('HomePageModule', []).factory('facebookConnect', function() {
    return new function() {
        this.askFacebookForAuthentication = function(fail, success) {
            FB.login(function(response) {
                if (response.authResponse) {
                    FB.api('/me', success);
                } else {
                    fail('User cancelled login or did not fully authorize.');
                }
            });
        }
    }
});

function ConnectCtrl(facebookConnect, $scope, $resource) {

    $scope.user = {}
    $scope.error = null;

    $scope.registerWithFacebook = function() {
        facebookConnect.askFacebookForAuthentication(
        function(reason) { // fail
            $scope.error = reason;
        }, function(user) { // success
            $scope.user = user
            $scope.$digest() // Manual scope evaluation
        });
    }
}

JSFiddle

以上示例的区别是什么?

  • 使用$q服务的原因情况是什么?
  • 它是如何工作的?

9
听起来你应该了解一下 Promise 是什么,以及它们为何通常被使用...... 它们不仅限于 Angular,并且有很多相关资料可供参考。 - charlietfl
1
@charlietfl,观点不错,但我希望得到一份复杂的回答,既涵盖了它们在一般情况下的使用方法,也讲述如何在 Angular 中使用。感谢您的建议。 - Mak
4个回答

401

这不会是对你问题的完整回答,但希望当你尝试阅读$q服务的文档时,这可以帮助你和其他人。我花了一段时间才明白它。

让我们暂时把AngularJS放在一边,只考虑Facebook API调用。这两个API调用都使用回调机制来通知调用者Facebook的响应何时可用:

  facebook.FB.api('/' + item, function (result) {
    if (result.error) {
      // handle error
    } else {
      // handle success
    }
  });
  // program continues while request is pending
  ...

这是处理JavaScript和其他语言中异步操作的标准模式。

当您需要执行一系列异步操作时,该模式存在一个大问题,其中每个后续操作都取决于前一个操作的结果。以下代码就是这样做的:

  FB.login(function(response) {
      if (response.authResponse) {
          FB.api('/me', success);
      } else {
          fail('User cancelled login or did not fully authorize.');
      }
  });

首先它尝试登录,仅在验证登录成功后才向Graph API发出请求。

即使在仅连接两个操作的情况下,事情也开始变得混乱起来。方法askFacebookForAuthentication接受一个回调函数,用于处理成功和失败的情况,但是当FB.login成功而FB.api失败时会发生什么?无论FB.api方法的结果如何,该方法始终调用success回调。

现在想象一下,您正在尝试编写一个由三个或更多异步操作正确处理每个步骤错误并且在几周后甚至对您自己易于理解的健壮序列。这是可能的,但很容易只是不断嵌套这些回调并在途中失去错误跟踪。

现在,让我们暂时将Facebook API搁置一旁,只考虑由$q服务实现的Angular Promises API。该服务实现的模式是尝试将异步编程转换回类似于简单语句线性系列的东西,具有在任何步骤处“抛出”错误并在最后处理它的能力,语义上类似于熟悉的try/catch块。

考虑这个人为的例子。假设我们有两个函数,其中第二个函数使用第一个函数的结果:

 var firstFn = function(param) {
    // do something with param
    return 'firstResult';
 };

 var secondFn = function(param) {
    // do something with param
    return 'secondResult';
 };

 secondFn(firstFn()); 

现在想象一下,firstFn和secondFn都需要很长时间才能完成,所以我们希望异步地处理这个序列。首先,我们创建一个新的deferred对象,它代表着一系列的操作:

 var deferred = $q.defer();
 var promise = deferred.promise;

promise属性代表链式操作最终的结果。如果你在创建一个 Promise 实例后立即将其打印出来,你会发现它只是一个空对象({})。目前还没有任何结果,继续执行下面的代码。

到目前为止,我们的 Promise 只代表链式操作的起点。现在让我们添加两个操作:

 promise = promise.then(firstFn).then(secondFn);
then方法添加一个步骤到链式调用中,然后返回一个表示扩展后的整个链式调用最终结果的新Promise。您可以添加任意数量的步骤。
目前为止,我们已经设置了函数链,但实际上还没有发生什么。通过调用deferred.resolve来启动这个过程,指定要传递给链中第一步的初始值。
 deferred.resolve('initial value');

然后...仍然没有任何变化发生。为了确保模型更改得到正确地观察,Angular在直到下一次调用$apply之前不会实际调用链中的第一步:

 deferred.resolve('initial value');
 $rootScope.$apply();

 // or     
 $rootScope.$apply(function() {
    deferred.resolve('initial value');
 });

那么错误处理呢? 到目前为止,我们只在每个步骤中指定了一个成功处理程序。 then 还接受一个可选的第二个参数作为错误处理程序。 下面是另一个更长的 promise 链式调用示例,这次包括错误处理:

 var firstFn = function(param) {
    // do something with param
    if (param == 'bad value') {
      return $q.reject('invalid value');
    } else {
      return 'firstResult';
    }
 };

 var secondFn = function(param) {
    // do something with param
    if (param == 'bad value') {
      return $q.reject('invalid value');
    } else {
      return 'secondResult';
    }
 };

 var thirdFn = function(param) {
    // do something with param
    return 'thirdResult';
 };

 var errorFn = function(message) {
   // handle error
 };

 var deferred = $q.defer();
 var promise = deferred.promise.then(firstFn).then(secondFn).then(thirdFn, errorFn);

正如您在此示例中所看到的,链中的每个处理程序都有机会将流量重定向到下一个错误处理程序而不是下一个成功处理程序。在大多数情况下,您可以在链的末尾拥有单个错误处理程序,但您也可以具有尝试恢复的中间错误处理程序。

为了快速回到您的示例(以及您的问题),我只想说它们代表了两种不同的方式来适应Facebook的基于回调的API到Angular的观察模型更改的方式。第一个示例将API调用包装在承诺中,可以添加到作用域中,并且被Angular的模板系统所理解。第二个采用更加强硬的方法,直接在作用域上设置回调结果,然后调用$scope.$digest()使Angular意识到来自外部源的更改。

这两个示例不能直接进行比较,因为第一个示例缺少登录步骤。但是,通常最好将与外部API的交互封装在单独的服务中,并将结果作为promise传递给控制器。这样,您就可以使您的控制器与外部关注点分开,并通过模拟服务更轻松地对其进行测试。


5
我认为这是一个很好的答案!对我来说,最重要的是描述承诺真正实际的一般情况。老实说,我希望得到另一个真实的例子(如Facebook那样),但我想这也可以。非常感谢! - Mak
2
一个替代使用多个 then 方法的方法是使用 $q.all。可以在这里找到有关它的快速教程。 - Bogdan
2
如果您需要等待多个独立的异步操作完成,那么$q.all是合适的选择。但如果每个操作都依赖于前一个操作的结果,则它不能替代链接操作。 - karlgold
1
很棒的回答@karlgold!我有一个问题。如果在最后一段代码片段中,将return 'firstResult'部分更改为return $q.resolve('firstResult'),会有什么区别? - technophyle
1
喜欢这个回答!+1 - Fergus
显示剩余4条评论

9
我原本期待一个复杂的答案,既能涵盖为什么一般会使用它们,又能说明如何在Angular中使用。
这是angular promises MVP(最小可行承诺)的plunk:http://plnkr.co/edit/QBAB0usWXc96TnxqKhuA?p=preview

来源:

(对于那些懒得点击链接的人)

index.html

  <head>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.1.5/angular.js"></script>
    <script src="app.js"></script>
  </head>

  <body ng-app="myModule" ng-controller="HelloCtrl">
    <h1>Messages</h1>
    <ul>
      <li ng-repeat="message in messages">{{ message }}</li>
    </ul>
  </body>

</html>

app.js

angular.module('myModule', [])

  .factory('HelloWorld', function($q, $timeout) {

    var getMessages = function() {
      var deferred = $q.defer();

      $timeout(function() {
        deferred.resolve(['Hello', 'world']);
      }, 2000);

      return deferred.promise;
    };

    return {
      getMessages: getMessages
    };

  })

  .controller('HelloCtrl', function($scope, HelloWorld) {

    $scope.messages = HelloWorld.getMessages();

  });

(我知道这并不能解决你的具体Facebook示例,但我发现下面的代码片段很有用)

参考链接:http://markdalgleish.com/2013/06/using-promises-in-angularjs-views/


更新于2014年2月28日:从1.2.0版开始,模板不再解析promise。 http://www.benlesh.com/2013/02/angularjs-creating-service-with-http.html

(此plunker示例使用的是1.1.5版本。)


据我所知,我们喜欢这样做是因为我们很懒。 - mkb
这帮助我理解了$q、deferred和链式.then函数调用,所以谢谢。 - aliopi

2
一个deferred代表一个异步操作的结果。它提供了一个接口,用于表示它所代表的操作的状态和结果。它还提供了一种获取相关promise实例的方式。

一个promise提供了一个与它相关的deferred进行交互的接口,因此,它允许感兴趣的方获取deferred操作的状态和结果。

当创建deferred时,它的状态为pending,并且没有任何结果。当我们resolve()或reject() deferred时,它的状态将更改为已解决或已拒绝。仍然可以在创建deferred后立即获取相关联的promise,并分配与其未来结果的交互。这些交互仅在deferred被拒绝或解决后才会发生。

1

在控制器中使用Promise,确保数据是否可用。

 var app = angular.module("app",[]);
      
      app.controller("test",function($scope,$q){
        var deferred = $q.defer();
        deferred.resolve("Hi");
        deferred.promise.then(function(data){
        console.log(data);    
        })
      });
      angular.bootstrap(document,["app"]);
<!DOCTYPE html>
<html>

  <head>
    <script data-require="angular.js@*" data-semver="1.3.0-beta.5" src="https://code.angularjs.org/1.3.0-beta.5/angular.js"></script>
  </head>

  <body>
    <h1>Hello Angular</h1>
    <div ng-controller="test">
    </div>
  </body>

</html>


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接