今天知乎上有网友提问—— 你对 Promise
的了解?
一、Promise 之前的时代——回调时代
假设我们用 getUser 来说去用户数据,它接收两个回调 sucessCallback 和 errorCallback:
function getUser(successCallback, errorCallback){
$.ajax({
url:'/user',
success: function(response){
successCallback(response)
},
error: function(xhr){
errorCallback(xhr)
}
})
}
看起来还不算复杂。
如果我们获取用户数据之后还要获取分组数组、分组详情等,代码就会是这样:
getUser(function(response){
getGroup(response.id, function(group){
getDetails(groupd.id, function(details){
console.log(details)
},function(){
alert('获取分组详情失败')
})
}, function(){
alert('获取分组失败')
})
}, function(){
alert('获取用户信息失败')
})
三层回调,如果再多一点嵌套,就是「回调地狱」了。
二、Promise 来了
Promise 的思路呢,就是 getUser 返回一个对象,你往这个对象上挂回调:
var promise = getUser()
promise.then(successCallback, errorCallback)
当用户信息加载完毕,successCallback 和 errorCallback 之一就会被执行。
把上面两句话合并成一句就是这样的:
getUser().then(successCallback, errorCallback)
如果你想在用户信息获取结束后做更多事,可以继续 .then:
getUser().then(success1).then(success2).then(success3)
请求成功后,会依次执行 success1、success2 和 success3。
如果要获取分组信息:
getUser().then(function(response){
getGroup(response.id).then(function(group){
getDetails(group.id).then(function(){
},error3)
},error2)
}, error1)
这种 Promise 写法跟前面的回调看起来其实变化不大。
真的,Promise 并不能消灭回调地狱,但是它可以使回调变得可控。你对比下面两个写法就知道了。
getGroup(response.id, success2, error2)
getGroup(response.id).then(success2, error2)
用 Promise 之前,你不能确定 success2 是第几个参数;
用 Promise 之后,所有的回调都是
.then(success, error)
这样的形式。
三、Promise 的用途
总结一下上面说的内容:Promise 对象是 JavaScript 的异步操作解决方案,为异步操作提供统一接口。它起到代理作用(proxy),充当异步操作与回调函数之间的中介,使得异步操作具备同步操作的接口。Promise 可以让异步操作写起来,就像在写同步操作的流程,而不必一层层地嵌套回调函数。
四、如何创建一个 new Promise
return new Promise((resolve,reject)=>{...})
- 任务成功调用resolve(result)
- 任务失败调用reject(error)
- resolve和reject 会再去调用成功和失败函数
- 使用 .then(success,fail)传入成功和失败函数 下面演示使用方法:
var promise = new Promise(function (resolve, reject) {
// ...
if (/* 异步操作成功 */){
resolve(value);
} else { /* 异步操作失败 */
reject(new Error());
}
});
上面代码中,Promise
构造函数接受一个函数作为参数,该函数的两个参数分别是resolve
和reject
。它们是两个函数,由 JavaScript 引擎提供,不用自己实现。
resolve
函数的作用是,在异步操作成功时调用,并将异步操作的结果,作为参数传递出去。
reject
函数的作用是,在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
五、如何使用 Promise.prototype.then(来源 MDN)
Promise 实例的then
方法,用来添加回调函数。
then
方法可以接受两个回调函数,第一个是异步操作成功时的回调函数,第二个是异步操作失败时的回调函数(该参数可以省略)。一旦状态改变,就调用相应的回调函数。
then方法可以链式使用。
p1
.then(step1)
.then(step2)
.then(step3)
.then(
console.log,
console.error
);
上面代码中,p1
后面有四个then
,意味依次有四个回调函数。只要前一步的状态变为fulfilled,就会依次执行紧跟在后面的回调函数。
最后一个then
方法,回调函数是console.log
和console.error
,用法上有一点重要的区别。console.log
只显示step3
的返回值,而console.error
可以显示p1
、step1
、step2
、step3
之中任意一个发生的错误。举例来说,如果step1
的状态变为rejected
,那么step2
和step3
都不会执行了(因为它们是resolved
的回调函数)。Promise 开始寻找,接下来第一个为rejected
的回调函数,在上面代码中是console.error
。这就是说,Promise 对象的报错具有传递性。
六、如何使用 Promise.all(来源 MDN)
方法返回一个Promise实例,此实例在 iterable 参数内所有的promise 都完成(resolved)时回调完成(resolve);如果参数中 promise有一个失败(rejected),此实例回调失败(reject),失败的原因是第一个失败promise的结果。
Promise2.all = function(arrP) {
let list = []
let len = 0
let hasErr = false
return new Promise2((resolve, reject) => {
for(let i = 0; i < arrP.length; i++) {
arrP[i].then( data=> {
list[i] = data
len++
len === arrP.length && resolve(list)
}, error => {
!hasErr && reject(error)
hasErr = true
})
}
})
}
七、如何使用 Promise.race(来源 MDN)
方法返回一个Promise实例,一旦迭代器中的某个 promise 完成(resolved)或失败(rejected),返回的 promise 就会 resolve 或 reject
Promise2.race = function(arrP) {
let hasValue = false
let hasError = false
return new Promise2((resolve, reject) => {
for(let i = 0; i < arrP.length; i++) {
arrP[i].then(data => {
!hasValue && !hasError && resolve(data)
hasValue = true
}, error => {
!hasValue && !hasError &&reject(error)
hasError = true
})
}
})
}
八、小结
Promise 的优点在于,让回调函数变成了规范的链式写法,程序流程可以看得很清楚。它有一整套接口,可以实现许多强大的功能,比如同时执行多个异步操作,等到它们的状态都改变以后,再执行一个回调函数;再比如,为多个回调函数中抛出的错误,统一指定处理方法等等。
而且,Promise 还有一个传统写法没有的好处:它的状态一旦改变,无论何时查询,都能得到这个状态。这意味着,无论何时为 Promise 实例添加回调函数,该函数都能正确执行。所以,你不用担心是否错过了某个事件或信号。如果是传统写法,通过监听事件来执行回调函数,一旦错过了事件,再添加回调函数是不会执行的。
Promise 的缺点是,编写的难度比传统写法高,而且阅读代码也不是一眼可以看懂。你只会看到一堆then,必须自己在then的回调函数里面理清逻辑。