什么是 Promise?Angular 里的 $q 和 Promise 到底是怎么样一种关系?在 Angular 里使用 $q 到底是怎么样一种体验?本文将为你一一解答。
原文链接:All about $q and Promises in Angular
原文作者:Todd Motto
译者:linpu.li
可能你曾经见过 $q
,或许已经在使用它了,但是可能你还没发现它有一些很棒的特性,比如 $q.all()
和 $q.race()
。这篇文章将深入 ES2015 的 Promise API 以及它是如何映射到 Angular 里的 $q
。换句话说,这篇文章全部是关于 $q
的,希望你能喜欢!
什么是 Promise?
Promise 是一个特殊类型对象,我们可以直接使用或者构造新的实例来处理一个异步任务。把它称作 Promise 是因为我们被“承诺”了在未来的某个时间点会得到一个结果。比如一个 HTTP 调用会在 200ms
或者 400ms
后完成,当完成之后就会有一个 Promise 执行。
一个 Promise 有三种状态:pending、resolved 和 rejected。在 Angular 里使用 $q
可以构造一个新的 Promise,但是先让我们来看看 ES2015 里的 Promise 对象来熟悉一下怎么创建它。
ES2015 里的 Promise
这里主要的内容是 Promise
和作为参数的 resolve
和 reject
。
let promise = new Promise((resolve, reject) => {
if (/* 某个异步任务顺利执行 */) {
resolve('Success!');
} else {
reject('Oops... something went wrong');
}
});
promise.then(data => {
console.log(data);
});
我们简单地调用了 new Promise()
,在其内部可以执行一个异步任务,这个任务可能封装了一个特别的 DOM 事件,或者甚至是封装了一些不是 Promise 对象的第三方库。
举个例子,封装一个假定的第三方库,叫做 myCallbackLib()
,它将提供一个 success 和 error 回调函数,我们可以在这个方法上构造一个 Promise,然后在相对应的位置去 resolve
和 reject
结果:
const handleThirdPartyCallback = someArgument => {
let promise = new Promise((resolve, reject) => {
// 假定某些不是 Promise 对象的第三方库接口
// 但调用完成后会执行回调函数
myCallbackLib(someArgument, response => {
// we can resolve it with the response
resolve(response);
}, reason => {
// we can reject it with the reason
reject(reason);
});
});
return promise;
};
handleThirdPartyCallback({ user: 101 }).then(data => {
console.log(data);
});
$q 构造函数
AngularJS 里的 $q
实现现在已经和原生的 ES2015 Promise
对象一样了,所以我们可以这么写:
let promise = $q((resolve, reject) => {
if (/* 某个异步任务顺利执行 */) {
resolve('Success!');
} else {
reject('Oops... something went wrong');
}
});
promise.then(data => {
console.log(data);
});
和之前代码唯一的区别就在于将 new Promise()
改成了 $q
,变得足够简单了。
更理想的情况是在一个 service 里实现它:
function MyService($q) {
return {
getSomething() {
return $q((resolve, reject) => {
if (/* 某个异步任务顺利执行 */) {
resolve('Success!');
} else {
reject('Oops... something went wrong');
}
});
}
};
}
angular
.module('app')
.service('MyService', MyService);
之后就可以将它注入一个 component 控制器:
const stuffComponent = {
template: `
<div>
{{ $ctrl.stuff }}
</div>
`,
controller(MyService) {
this.stuff = [];
MyService.getSomething()
.then(data => this.stuff.unshift(data));
}
};
angular
.module('app')
.component('stuffComponent', stuffComponent);
或者作为一个 bindings
属性在一个路由组件中使用,并映射到一个路由处理对象:
const stuffComponent = {
bindings: {
stuff: '<'
},
template: `
<div>
{{ $ctrl.stuff }}
</div>
`,
controller(MyService) {
// your stuff already available
console.log(this.stuff);
}
};
const config = $stateProvider => {
$stateProvider
.state('stuff', {
url: '/stuff',
component: 'stuffComponent',
resolve: {
// resolve maps the `MyService` promise response
// Object across to `stuff` property, making it
// available as a binding inside the .component()
stuff: MyService => MyService.getSomething()
}
});
};
angular
.module('app')
.config(config)
.component('stuffComponent', stuffComponent);
什么时候使用 $q
目前为止我们都只是看了一些假定的例子,下面的实现是我将一个 XMLHttpRequest
对象封装成了一个基于 Promise 的解决方案,这种类型的实现应该是你创建自己的 $q
Promise 仅有的真实原因吧:
let getStuff = $q((resolve, reject) => {
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
resolve(JSON.parse(xhr.responseText));
} else {
reject(JSON.parse(xhr.responseText));
}
}
};
xhr.open('GET', '/api/stuff', true);
xhr.send();
});
getStuff.then(data => {
console.log('Boom!', data);
});
请注意,这并不是在倡导你创建和使用 XMLHttpRequest
,在 Angular 里就使用 $http
,它已经为你创建并返回了一个 Promise 对象:
function getStuff() {
return $http
.get('/api/stuff');
.then(data => {
console.log('Boom!', data);
});
}
getStuff().then(data => {
console.log('Boom!', data);
});
这还意味着,你不能而且不应该像下面这样做,因为下面相当于从一个已经存在的 Promise 对象里创建一个 Promise 对象:
function getStuff() {
// 不要这么做!
let defer = $q.defer();
$http
.get('/api/stuff');
.then(response => {
// 不要这么做!
$q.resolve(response);
}, reason => {
// 不要这么做!
$q.reject(reason);
});
return defer.promise;
}
getStuff().then(data => {
console.log('Boom!', data);
});
黄金法则:只对本身没有 Promise 的东西使用 $q
!虽然只在这种情况下创建 Promise,但是你可以对其他的 Promise 使用一些其他的方法,比如 $q.all()
和 $q.race()
。
$q.defer()
使用 $q.defer()
只是 $q
作为构造函数的另外一种风格和原始实现。假设下面的代码,改自之前使用 service 的例子:
function MyService($q) {
return {
getSomething() {
let defer = $q.defer();
if (/* some async task is all good */) {
defer.resolve('Success!');
} else {
defer.reject('Oops... something went wrong');
}
return defer.promise;
}
};
}
$q.when() / $q.resolve()
当你想要立即从一个非 Promise 对象中处理一个 Promise 的时候就可以使用 $q.when()
或者 $q.resolve()
(它们是一样的,$q.resolve()
是 $q.when()
的一个别名,为了符合 ES2015 Promise 的命名约定),举个例子:
$q.when(123).then(res => {
// 123
console.log(res);
});
$q.resolve(123).then(res => {
// 123
console.log(res);
});
注意:$q.when()
也是和 $q.resolve()
一样的。
$q.reject()
使用 $q.reject()
会立即拒绝掉一个 Promise,这么做是为了方便一些情况做处理,比如在 HTTP 拦截器没有任何返回的时候,就可以返回一个拒绝掉的 Promise 对象:
$httpProvider.interceptors.push($q => ({
request(config) {...},
requestError(config) {
return $q.reject(config);
},
response(response) {...},
responseError(response) {
return $q.reject(response);
}
}));
$q.all()
有的时候你可能需要一次性处理多个 Promise,通过 $q.all()
就可以轻易实现,只需传递进 Promise 的数组或者对象,接着就会在所有 Promise 都处理完后调用 .then()
方法:
let promiseOne = $http.get('/api/todos');
let promiseTwo = $http.get('/api/comments');
// Promise 数组
$q.all([promiseOne, promiseTwo]).then(data => {
console.log('Both promises have resolved', data);
});
// Promise 对象哈希
// 这是 ES2015 对 { promiseOne: promiseOne, promiseTwo: promiseTwo } 的简写
$q.all({
promiseOne,
promiseTwo
}).then(data => {
console.log('Both promises have resolved', data);
});
$q.race()
$q.race()
是 Angular 里面的一个新方法,和 $q.all()
类似,但是它只会返回第一个处理完成的 Promise 给你。假定 API 调用 1 和 API 调用 2 同时执行,而 API 调用 2 在 API 调用 1 之前处理完成,那么你就只会得到 API 调用 2 的返回对象。换句话说,最快(处理完成)的 Promise 会赢得返回对象的机会:
let promiseOne = $http.get('/api/todos');
let promiseTwo = $http.get('/api/comments');
// Promise 数组
$q.race([promiseOne, promiseTwo]).then(data => {
console.log('Fastest wins, who will it be?...', data);
});
// Promise 对象哈希
// 这是 ES2015 对 { promiseOne: promiseOne, promiseTwo: promiseTwo } 的简写
$q.race({
promiseOne,
promiseTwo
}).then(data => {
console.log('Fastest wins, who will it be?...', data);
});
结论
使用 $q
对非 Promise 的对象或回调构造 Promise,利用 $q.all()
和 $q.race()
处理已存在的 Promise。
还想看更多内容,$q 文档送上。
本文同步于我的个人博客,欢迎大家讨论指正。