翻译:JavaScript Promises and AngularJS $q Service

翻译:JavaScript Promises and AngularJS $q Service

原文:http://www.webdeveasy.com/javascript-promises-and-angularjs-q-service/

原文时间:2014年9月30号

      一个promise(延缓)是处理异步开发简单而强大的方法。CommonJS 维基百科列出了 几个promise模式的实施提议。AngularJS自己的promise实现方法是受kris Kowal's Q的方法启发的。在这篇文章中我会介绍promises,它的目的和怎么通过AngularJS $q的promise服务开发的教程。


      Promise(延缓)目的

    在JavaScript中,异步方法通常通过调用回调方法来实现成功或失败的处理。比如说浏览器的地理位置api,获取地理坐标时就需要成功或者失败的回调方法。

function success(position) {
  var coords = position.coords;
  console.log('Your current position is ' + coords.latitude + ' X ' + coords.longitude);
}

function error(err) {
  console.warn('ERROR(' + err.code + '): ' + err.message);
}

navigator.geolocation.getCurrentPosition(success, error);

      另一个例子就是xhr请求(ajax请求),它有一个onreadystatechange回调方法,当readyState改变时就会调用它。

var xhr = new window.XMLHttpRequest();
xhr.open('GET', 'http://www.webdeveasy.com', true);
xhr.onreadystatechange = function() {
    if (xhr.readyState === 4) {
        if (xhr.status === 200) {
            console.log('Success');
        }
    }
};
xhr.send();

      Javascript中还有许多其他的异步例子,下面讨论麻烦的多个异步方法运行

 串行(无尽金字塔)

    假设现在有N种异步方法需要串行运行:async1(success, failure)async2(success, failure), …, asyncN(success, failure),一个接一个执行直到成功,每个方法都有成功和失败的回调,那么代码就是这样:

async1(function() {
    async2(function() {
        async3(function() {
            async4(function() {
                ....
                    ....
                        ....
                           asyncN(null, null);
                        ....
                    ....
                ....
            }, null);
        }, null);
    }, null);
}, null); 

    这就是著名的回调金字塔(callback pyramid of doom)。虽然还有更好的写法(把回调流分隔成函数),但是这种方式还是很难读懂和维护。

并行

    假设我们有N个异步方法,async1(success, failure)async2(success, failure), …, asyncN(success, failure) ,我们需要让他们并行运行,再在最后弹出一个消息。每个方法都有成功和失败的回调,那么代码就是这样:

var counter = N;

function success() {
    counter --;
    if (counter === 0) {
        alert('done!');
    }
}

async1(success);
async2(success);
....
....
asyncN(success);

      我们声明了一个变量counter,让它的值为N,每当一个方法成功了,就减一,然后检测是否为零,也就是是否执行到最后一个了。这种方法使用起来既麻烦又不利于维护,特别是当每个异步方法还有参数要传到success()方法里的时候,那样我们还要保存每次运行的结果。

      在上面两个例子里,在一个异步操作过程中,我们必须要指定成功的回调。也就是说,当我们用回调时,异步操作需要一个引用来进行下一步,但是它的下一步操作也许是与自己无关的。这就导致了很难重复使用和测试的紧密耦合模块和服务。


 什么是Promise和Deferred

      deferred表示一个异步操作的结果。它含有操作的结果和提供一个可以用来标记状态的接口。它还提供一个连接promise实例的途径。

      promise提供一可以和相关deferred互动的接口,这样就可以获取到deferred的结果和状态了。

      当创建一个deferred,它的状态是等待中(pending),这时它没有值。当我们resolve()或者reject()这个deferred的时候,它把状态改为已解决(resolved)或者已拒绝(rejected)。我们在创建一个deferred后还是可以立刻获取到promise,设置给它将来的操作结果分配互动操作。这些互动操作只有当deferred解决或者拒绝后才会执行。

      如果需要处理多个异步,通过promises我们也可以容易的创建异步操作,即使我们还没决定下一步要做什么。这就是为什么耦合是松散的。一个异步操作不需要下一步的动作,它只需要知道什么时候准备好。

      defferred提供方法改变一个操作的状态,而promise只提供需要的方法来处理和解决状态,而不是改变状态的方法。

      在不同语言中(JavaScript,JAVA,C++,Python等等)和框架(NodeJS,JQuery)有许多promise实现方法。AngularJS的实现方法就是$q这个服务。


      怎么使用Deferrers和Promises

      在理解了promises和他们的目的之后,现在我们来看看怎么使用他们。之前提到,promises有多种实现方法,因此,不同的方法对应不同的用途。这里将会介绍AngularJS中的promise实现-$q服务。如果你使用的是其他实现方法,别担心,我会阐述大部分方法,即使没说到,你也可以找到类似的。

     基本用法

     首先,我们先创建一个deferred

var myFirstDeferred = $q.defer();

  简单吧,myFirstDeferred代表一个异步操作结束后可以被解决(resolved)或者拒绝的(rejected)deffered。假设我们有一个带有有成功和失败的回调方法的异步操作:async(success, failure)。当async执行后,我们想要处理它的结果(返回值或者错误信息)。

async(function(value) {
    myFirstDeferred.resolve(value);
}, function(errorReason) {
    myFirstDeferred.reject(errorReason);
});

既然$q的处理方法(resolve(),reject())不需要结果就可以执行,我们也可以简化一下:(注:这里是一个异步前提,还没用到AngularJS特性):

async(myFirstDeferred.resolve, myFirstDeferred.reject);

然后我们来处理promise,把promise从myFirstDeferred中拿出来,这样给它指定回调的操作就简单了(注:这里没用到async呢,这里是AngularJS中$q的用法,then,then代表成功,失败回调,详细往下看):

var myFirstPromise = myFirstDeferred.promise;

myFirstPromise
    .then(function(data) {
        console.log('My first promise succeeded', data);
    }, function(error) {
        console.log('My first promise failed', error);
    });

记住我们可以在创建deferred后直接指定回调方法,即使在调用async()之前,想指定什么就指定什么:

var anotherDeferred = $q.defer();
anotherDeferred.promise
    .then(function(data) {
        console.log('This success method was assigned BEFORE calling to async()', data);
    }, function(error) {
        console.log('This failure method was assigned BEFORE calling to async()', error);
    });

async(anotherDeferred.resolve, anotherDeferred.reject);

anotherDeferred.promise
    .then(function(data) {
        console.log('This ANOTHER success method was assigned AFTER calling to async()', data);
    }, function(error) {
        console.log('This ANOTHER failure method was assigned AFTER calling to async()', error);
    });

如果async()成功,两个成功的回调都会触发。反之亦然。我推荐的做法是把异步操作封装下,它返回一个promise。这样的话既可以指定回调方法,又不用担心会干涉deferred的状态。

function getData() {
    var deferred = $q.defer();
    async(deferred.resolve, deferred.reject);
    return deferred.promise;
}
...
... // Later, in a different file
var dataPromise = getData()
...
...
... // Much later, at the bottom of that file :)
dataPromise
    .then(function(data) {
        console.log('Success!', data);
    }, function(error) {
        console.log('Failure...', error);
    });

至此,我们通过promises,指定了成功和失败的回调。当然,我们也可以只指定成功或只指定失败回调。

promise.then(function() {
    console.log('Assign only success callback to promise');
});

promise.catch(function() {
    console.log('Assign only failure callback to promise');
    // 这是promise.then(null, errorCallback)的缩写
});

 只调用promise.then()会只指定成功回调,调用proise.catch会调用失败回调。catch()其实是promise.then(null,errorCallback)的缩写。

有时候我们想触发一个回调,无论是成功还是失败,我们就可以用promise.finally()

promise.finally(function() {
    console.log('Assign a function that will be invoked both upon success and failure');
});

 这个等同于:

var callback = function() {
    console.log('Assign a function that will be invoked both upon success and failure');
};
promise.then(callback, callback);

绑定变量和promises

假设我们有一个返回一个promise的异步方法async(),我们来看下这段有趣的代码:

var promise1 = async();
var promise2 = promise1.then(function(x) {
    return x+1;
});

你可以看粗来,promise1.then()返回另一个promise,我把它命名为promise2。当promise1办完x的事情后,成功回调就会返回x+1。这时promise2就会代入x+1运行。

另一个简单的例子:

var promise2 = async().then(function(data) {
    console.log(data);
    ... // Do something with data
    // Returns nothing!
});

这里,当从async()返回的promise解决后,成功回调就会触发,promise2就会代入undefined解析(成功回调没有返回值)。

你可以看粗来,promises可以带上参数,而且总是在回调函数出发后代入返回值运行。

为了演示一下,这里用promise做了个比较傻的例子(其实没必要用promises):

function async(value) {
    var deferred = $q.defer();
    var asyncCalculation = value / 2;
    deferred.resolve(asyncCalculation);
    return deferred.promise;
}

var promise = async(8)
    .then(function(x) {
        return x+1;
    })
    .then(function(x) {
        return x*2;
    })
    .then(function(x) {
        return x-1;
    });

promise.then(function(x) {
    console.log(x);
});

这个promises链开始的时候召唤async(8),async(8)运行后就给了promise一个4,4通过层层回调得到了9。((8 / 2 + 1) * 2 - 1))。

如果我们用一个promise作为参数(不用变量)呢?假设我们有两个异步方法,async1()和async2(),每个都返回一个promise。看看:

var promise = async1()
    .then(function(data) {
        // Assume async2() needs the response of async1() in order to work
        var async2Promise = async2(data);
        return async2Promise;
});

恩,这次的成功回调扮演另一个异步操作,同时返回一个promise。从async1().then()返回的是一个promise,现在可以通过async2Promse来解析运行。

async2()的参数是async1()运行后的值,async2()返回的是一个promise,我们也可以这样写:

var promise = async1()
    .then(async2);

恩,下一个例子(还是async1()和async2())。

// 我们意淫一下这是真的异步方法
function async1(value) {
    var deferred = $q.defer();
    var asyncCalculation = value * 2;
    deferred.resolve(asyncCalculation);
    return deferred.promise;
}
function async2(value) {
    var deferred = $q.defer();
    var asyncCalculation = value + 1;
    deferred.resolve(asyncCalculation);
    return deferred.promise;
}

var promise = async1(10)
    .then(function(x) {
        return async2(x);
    });

promise.then(function(x) {
    console.log(x);
});

 首先,我们调用async(10),经过运算得到20,然后返回一个promise。接着成功回调触发,async2(20)返回一个promise。最后输出21。同样功能却更易读的代码:

function logValue(value) {
    console.log(value);
}

async1(10)
    .then(async2)
    .then(logValue);

很容易看出我们先调用了async1(),然后我们调用了async2(),最后我们调用了logValue()。每个方法都用前一个方法resolved值作为参数。合理的命名也能帮助我们理解。

前面所有的例子都是理想化的,因为都是调用成功后的。但是以防一个promise因为什么失败了(rejected),绑定的promise也会失败(rejected)。

// Let's imagine those are really asynchronous functions
function async1(value) {
    var deferred = $q.defer();
    var asyncCalculation = value * 2;
    deferred.resolve(asyncCalculation);
    return deferred.promise;
}
function async2(value) {
    var deferred = $q.defer();
    deferred.reject('rejected for demonstration!');
    return deferred.promise;
}

var promise = async1(10)
    .then(function(x) {
        return async2(x);
    });

promise.then(
    function(x) { console.log(x); },
    function(reason) { console.log('Error: ' + reason); });

这段代码的结果是Error: rejected for demonstration!。Promises可以绑定promise,根据绑定的promise来成功或者失败(通过绑定的promiser的resolve或者reject的值)。

一个例子:

async1()
    .then(async2)
    .then(async3)
    .catch(handleReject)
    .finally(freeResources);

在这个例子中,我们一个接一个的调用async1(),async2()和async3()。如果其中一个出现意外,成功调用链就会停止然后运行handleReject()。最后,无论成功与否都会触发freeResources()。比如说,async2()被rejected了,async3()就不会运行,

handleReject()会代入async2()被rejected的结果运行,最后触发freddResources()。

有用的方法

$q有几个帮助方法可以帮助我们使用promises。就像我说的,其他几种promises实现也有一样的方法,也许只是换了个名字。

有时候我们需要返回一个被拒绝的(rejected)promise。不用创建一个新的promise然后拒绝(rejected)它,我们可以使用$q.reject(reason)。$q.reject(reason)返回一个带有被拒绝原因的promise:

var promise = async().then(function(value) {
        if (isSatisfied(value)) {
            return value;
        } else {
            return $q.reject('value is not satisfied');
        }
    }, function(reason) {
        if (canRecovered(reason)) {
            return newPromiseOrValue;
        } else {
            return $q.reject(reason);
        }
    });

如果async()通过一个合适的值接受了,这个值被绑定了,且会被promise接受。

posted @ 2014-11-12 11:53  登记造册  阅读(249)  评论(0编辑  收藏  举报