Angular学习笔记(12)—promise

什么是promise

promise是一种用异步方式处理值(或非值)的方法。promise是对象,代表了一个函数最终可能的返回值或者抛出的异常。
习惯上,JS使用闭包或者回调来响应非同步的有意义的数据,比如页面加载之后的XHR请求。我们可以跟数据进行交互,就好像它已经返回了一样,而不需要依赖于回调函数的触发。
回调使得调用不一致,得不到保证,当依赖于其他回调时,它们篡改代码的流程,通常会让调试变得非常难。每一步调用之后,都需要显式处理错误。
在执行异步方法时触发一个函数,然后期待一个回调能运行起来。与之不同的是,promise提供了另外一种抽象:这些函数返回promise对象。

 // 示例回调代码
 User.get(fromId, {
     success: function(err, user) {
         if (err) return {error: err}; 
         user.friends.find(toId, function(err, friend) { 
             if (err) return {error: err}; 
             user.sendMessage(friend, message, callback); 
         });
     },
     fail: function(err) {
         return {error: err}
     }
 });

这个回调金字塔已经失控了,而且我们还没有加入健壮的错误处理代码。此外,在被调用的回调内部,也需要知道参数的顺序。
刚才基于promise版本的代码看上去更接近于:

User.get(fromId)
    .then(function(user) {
        return user.friends.find(toId);
    }, function(err) {
    // 没找到用户
})
.then(function(friend) {
    return user.sendMessage(friend, message); 
}, function(err) {
    // 用户的朋友返回了异常
})
.then(function(success) {
    // user was sent the message
}, function(err) {
    // 发生错误了
});

代码不仅仅是可读性变高了,也更容易理解了。我们可以保证回调是一个值,而不用处理回调接口。

为什么使用promise

promise让异步函数看上去像同步的。基于同步函数,我们可以按照预期来捕获返回值和异常值。
可以在程序中的任何时刻捕捉错误,并且绕过依赖于程序异常的后续代码。
因此,使用promise的目的是:获得功能组合和错误冒泡能力的同时,保持代码异步运行的能力。
promise是头等对象,自带了一些约定。

  • 只有一个resolve或者reject会被调用到:
    • resolve被调用时,带有一个履行值;
    • reject被调用时要带一个拒绝原因。
  • 如果promise被执行或者拒绝了,依赖于它们的处理程序仍然会被调用;
  • 处理程序总是会被异步调用。

此外,可以把promise串起来,并且允许代码以通常运行的方式来处理。从一个promise冒出的异常会贯穿整个promise链。promise总是异步执行的,可以放心使用,无需担心它们会阻塞应用的其他部分。

AngularJS中的promise

AngularJS的事件循环给予了AngularJS特有的能力,能在$rootScope.$evalAsync阶段中执行promisepromise会坐等$digest运行循环结束。这件事让我们能毫无压力地把promise的结果转换到视图上。它也能让我们不加思考地把XHR调用的结果直接赋值到$scope对象的属性上。
例子:从GitHub上返回一组针对AngularJS的开放pull请求。

<h1>Open Pull Requests for Angular JS</h1>
<ul ng-controller="DashboardController">
    <li ng-repeat="pr in pullRequests">
        {{ pr.title }}
    </li>
</ul>

如果有服务返回了一个promise,可以在.then()方法中与这个promise交互,它允许我们修改作用域上的任意变量,放置到视图上,并且期望AngularJS会为我们执行它。

angular.module('myApp', []) 
.controller('DashboardController', [
    '$scope', 'GithubService', 
        function($scope, UserService) { 
            // GithubService的getPullRequests()方法 
            // 返回了一个promise 
            User.getPullRequests(123) 
             .then(function(data) { 
                $scope.pullRequests = data.data; 
        });
}]);

当对getPullRequests的异步调用返回时, 在.then()方法中就可以用$scope.pullRequests这个值了,然后它会更新$scope.pullRequests数组。

如何创建一个promise

在AngularJS中创建promise,可以使用内置的$q服务。$q服务在它的deferred API中提供了一些方法。
首先,需要把$q服务注入到想要使用它的对象中。

angular.module('myApp',[]).factory('GithubService',['$q',function($q) { 
  // 现在就可以访问到$q库了
}]);

要创建一个deferred对象,可以调用defer()方法。

var deferred = $q.defer();

deferred对象暴露了三个方法,以及一个可以用于处理promisepromise属性。

  • resolve(value):resolve函数用这个值来执行deferred promise
deferred.resolve({name: "Ari", username: "@auser"});
  • reject(reason)
    这个方法用一个原因来拒绝deferred promise。它等同于使用一个“拒绝”来执行一个promise
deferred.reject("Can't update user");
// 等同于
deferred.resolve($q.reject("Can't update user"));
  • notify(value):这个方法用promise的执行状态来进行响应。

如果我们要从promise返回一个状态,可以使用notify()函数来传送它。假设我们想要从一个promise创建多个长时间运行的请求。可以调用notify函数发回一个过程通知。

.factory('GithubService', function($q, $http) {
    // 从仓库获取事件
    var getEventsFromRepo = function() {
        // 任务
    }
    var service = {
        makeMultipleRequests: function(repos) { 
            var d = $q.defer(), 
                percentComplete = 0,
                output = [];
            for (var i = 0; i < repos.length; i++) { 
                output.push(getEventsFromRepo(repos[i])); 
                percentComplete = (i+1)/repos.length * 100; 
                d.notify(percentComplete);
            }
            d.resolve(output);
            return d.promise; 
        }
    }
    return service;
});

有了GithubService对象上的这个makeMultipleRequests()函数,每次获取和处理一个仓库时,都会收到一个过程通知。
可以在我们对promise的使用中用到这个通知,在用promise时加上第三个函数调用。

.controller('HomeController',
    function($scope, GithubService) { 
        GithubService.makeMultipleRequests([ 
            'auser/beehive', 'angular/angular.js'
        ])
        .then(function(result) {
            // 处理结果
        }, function(err) {
            // 发生错误了
        }, function(percentComplete) { 
            $scope.progress = percentComplete; 
        });
});

可以在deferred对象上以属性的方式访问promisedeferred.promise
上面这个例子展示了如何创建一个函数用于响应promise,看上去可能类似于下面这些GithubService上的方法。

angular.module('myApp', [])
    .factory('GithubService', [
        '$q', '$http',
        function($q, $http) {
            var getPullRequests = function() {
                var deferred = $q.defer();
                // 从Github获取打开的angularjs pull请求列表 
                $http.get('https://api.github.com/repos/angular/angular.js/pulls') 
                .success(function(data) { 
                    deferred.resolve(data);
                })
                .error(function(reason) { 
                    deferred.reject(reason);
                })
                return deferred.promise;
            }
            return { // 返回工厂对象 
                getPullRequests: getPullRequests 
            };
}]);

现在我们就可以用promise API来跟getPullRequests() promise交互。在上面这个service的实例中,可以用两种不同方式跟promise交互。

  • then(successFn,errFn,notifyFn)
    无论promise成功还是失败了,当结果可用之后,then都会立刻异步调用successFn或者errFn。这个方法始终用一个参数来调用回调函数:结果,或者是拒绝的理由。
    promise被执行或者拒绝之前,notifyFn回调可能会被调用0到多次,以提供过程状态的提示。
    then()方法总是返回一个新的promise,可以通过successFn或者errFn这样的返回值执行或者被拒绝。它也能通过notifyFn提供通知。
  • catch(errFn)
    这个方法就只是个帮助函数,能让我们用.catch(function(reason){})取代err回调。
$http.get('/repos/angular/angular.js/pulls')
.catch(function(reason) {
deferred.reject(reason);
});
  • finally(callback)
    finally方法允许我们观察promise的履行或者拒绝,而无需修改结果的值。当我们需要释放一个资源,或者是运行一些清理工作,不管promise是成功还是失败时,这个方法会很有用。
    我们不能直接调用这个方法,因为finally是IE中JS的一个保留字。纠结到最后,只好这样调用它了:
promise['finally'](function() {});

AngularJS的$q deferred对象是可以串成链的,这样即使是then,返回的也是一个promise。这个promise一被执行,then返回的promise就已经是resolved或者rejected的了。
这些promise也就是AngularJS能支持$http拦截器的原因。
$q服务类似于原始的Kris Kowal的Q库:
(1) $q是跟Angular的$rootScope模型集成的,所以在Angular中,执行和拒绝都很快。
(2) $q promise是跟Angular模板引擎集成的,这意味着在视图中找到的任何promise都会在视图中被执行或者拒绝。
(3) $q很小,所以没有包含Q库的完整功能。

链式请求

then方法在初始promise被执行之后,返回一个新的派生promise。这种返回形式可以把另一个then接在初始的then方法结果之后。

 // 一个响应promise的服务 
 GithubService.then(function(data) {
     var events = [];
     for (var i = 0; i < data.length; i++) { 
         events.push(data[i].events);
     }
     return events;
 }).then(function(events) {
     $scope.events = events;
});

在本例中,我们可以创建一个执行链,它允许我们中断基于更多功能的应用流程,可以籍此导向不同的结果。这个中断能让我们在执行链的任意时刻暂停或者推迟promise的执行。这个中断也是$http服务实现请求和响应拦截器的方式。
$q库自带了几个不同的有用方法。

all(promises)

  如果我们有多个promise,想要把它们合并成一个,可以使用$q.all(promises)方法来把它
们合并成一个promise。这个方法带有一个参数。

  • promises(数组或者promise对象):一个promise数组或者promisehash

all()方法返回单个promise,会执行一个数组或者一个散列的值。每个值会响应promise散列中的相同序号或者键。如果任意一个promise被拒绝了,结果的promise也会被拒绝。

defer()

defer()方法创建了一个deferred对象,它没有参数,返回deferred对象的一个实例。

reject(reason)

这个方法创建了一个promise,被以某一原因拒绝执行了。它专门用于让我们能在一个promise链中转发拒绝的promise,类似JS中的throw。在同样意义上,我们能在JS中捕获一个异常,也能够转发这个拒绝,我们需要把这个错误重新抛出。可以通过$q.reject(reason)来做到这点。
这个方法带有单个参数:

  • reason(常量、字符串、异常、对象):拒绝的原因。

reject()方法返回一个已经用某个原因拒绝的promise

when(value)

when()函数把一个可能是值或者能接着thenpromise包装成一个$q promise。这样我们就能处理一个可能是也可能不是promise的对象。
when()函数有一个参数:value,该参数是个值,或者是promise
when()函数返回了一个promise,我们可以像使用其他promise一样使用它。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容