一、为什么要有Promise
在研究 Promise 之前我们先来看一段Jquery ajax 的操作
// 为方便表述我们给这段代码取名为 ajaxA
$.post('/your/api/a',{'paramKey':'value'},function(data,status){
if('success' == status){
console.log(data);//获取到数据
}
})
以上代码用ajax对服务端发起了请求并获取返回的数据 data
,网络请求需要一定时长,所以我们ajax请求的回调中处理数据。如上述 console.log(data)。如果感觉还不够直观,请看下面的错误示范
// 为方便表述我们称下面这段代码为 代码A
let targetData = 1;
// setTimeout 模拟 ajax 的请求延迟
let t = setTimeout(() => {
clearTimeout(t);
targetData = 10;
}, 1000);
console.log(targetData);//1
以上代码直观表示只能在回调中处理数据。那么假设我们的 ajaxA 返回的数据,又得继续当作 ajaxB 的参数再一次请求数据。那么代码将会变成以下这样子
// ajaxA
$.post('/your/api/a',{'paramKey':'value'},function(data,status){
if('success' == status){
console.log(data);//获取到数据
// ajaxB 以 ajaxA 返回的数据为参数
$.post('/your/api/b',data,function(data,status){
if('success' == status){
console.log(data);//获取到数据
//假设还有,那就俄罗斯套娃继续往下套
}
})//END ajaxB
}
})
这才是两层的代码,就已经显得十分混乱了,当然你可以封装函数取调用,但是治标不治本,依然混乱。我们暂且极端地假设有10层或者更多层回调,首先代码的可阅读性就会变得很差,其次这多层中某一层出问题或者失败都需要有不同的处理,因此十分混乱,简直就是地狱了。这就是JS中的“回调地狱”。
二、Promise 怎么解决回调地狱
如何优雅解决上述问题呢,ES6中提供了 Promise,Promise 的出现使得我们能像同步任务一样来编写异步任务的代码。我们先来改写一下“代码A”
let targetData = 1;
//把setTimeout定义在 new promise 中
let promise = new Promise((resolve, reject) => {
let t = setTimeout(() => {
clearTimeout(t);
targetData = 10;
resolve();//将Promise 的状态改为成功 fulfilled
}, 1000);
})
//在 promise.then 获取 targetData
promise.then(()=>{
console.log('targetData',targetData)
})
这就特别像是在写同步任务的代码了。我们来写段代码演示多层回调有什么区别
function promisePost(url, data) {
return new Promise((resolve, reject) => {
let t = setTimeout(function () {
clearTimeout(t);
//假设全是成功的
resolve(data);
}, 200)
})
}
//请求A
promisePost('url A', 'data A').then(res => {
console.log(res);
return promisePost('url B', 'data B');//为方便表述取名为 promiseB
}).then(res => { //注意这里的 then 是 promiseB 的
console.log(res);
return promisePost('url C', 'data C');//为方便表述取名为 promiseC
}).then(res => { //注意这里的 then 是 promiseC 的
console.log(res);
return promisePost('url D', 'data D');//为方便表述取名为 promiseD
}).then(res => {//注意这里的 then 是 promiseC 的
console.log(res);
})
打印结果如下
data A
data B
data C
data D
链式调用就比嵌套更具有条理性了,代码的可阅读性也得到提高。
三、Promise 如何捕获异常
上面代码我们有多层回调依赖,假如其中某一环出现错误,该怎么处理。
我们先改造 promisePost 代码
function promisePost(url, data) {
return new Promise((resolve, reject) => {
let t = setTimeout(function () {
clearTimeout(t);
//模拟成功或者失败
Math.random() > 0.5 ? resolve(data) : reject('失败的URL: ' + url);
}, 200)
})
}
然后在链式调用的最后添加一个catch
//请求A
promisePost('url A', 'data A').then(res => {
console.log(res);
return promisePost('url B', 'data B');//为方便表述取名为 promiseB
}).then(res => { //注意这里的 then 是 promiseB 的
console.log(res);
return promisePost('url C', 'data C');//为方便表述取名为 promiseC
}).then(res => { //注意这里的 then 是 promiseC 的
console.log(res);
return promisePost('url D', 'data D');//为方便表述取名为 promiseD
}).then(res => {//注意这里的 then 是 promiseC 的
console.log(res);
}).catch(err => {
console.log(err);
})
取其中一次打印结果举例
data A
data B
失败的URL: url C
因为是随机模拟成功或者失败,所以多刷新几次会发现规律,链式调用中遇到失败的promise 则不会继续调用下一个 promise 的 then,catch 只需要在链式的最后添加,利用错误冒泡机制一层一层往上传,直到被catch捕获。不得不说确实优雅!
四、Promise 的其他补充
Promise 的 三种状态
pending
:进行中,表示Promise还在执行阶段
fulfilled
:成功状态,表示 promise 成功执行
rejected
:拒绝状态,表示promise被拒绝,或者说失败
promise 只有能是其中一种状态,可以从pending变成其他两种状态,之后状态就会固定,无法在次发生更改。
- 在执行 resolve 时,会将Promise 的状态改为 fulfilled,并执行then方法。resolve 可以携带参数,参数将作为返回值返回。
- 在执行 reject 时,会将Promise的状态改为 rejected,并会被 catch 捕获 reject 可以携带参数,参数将作为返回值返回。
除了promise.then 和 promise.catch 外还有 promise.finally。finally 无论 promise 的状态是 fulfilled 还是 rejected,到最后都会执行。可以在这里做一些资源回收或者统一业务的处理。
Promise.all 方法
Promise.al提供了并行执行异步任务的能力并且会统一返回结果,每个结果为数组的一个元素
Promise.all([
new Promise((resolve,reject)=>{
resolve('result A')
}),
new Promise((resolve,reject)=>{
resolve('result B')
})
]).then(res=>{
console.log(res)// ['result A', 'result B']
})
假如有时间差,结果会不会不同,改造代码如下
Promise.all([
new Promise((resolve, reject) => {
let t = setTimeout(() => {
clearTimeout(t)
resolve('result A')
}, 500);//慢
}),
new Promise((resolve, reject) => {
let t = setTimeout(() => {
clearTimeout(t)
resolve('result B')
}, 100);//快
})
]).then(res => {
console.log(res)// ['result A', 'result B']
})
结果依然是 [‘result A’, ‘result B’] ,结果有序,利用这个特征就可以做一些有顺序的结果处理。
Promise.race 方法
与 all 返回结果不同的是race只返回最快的 primise 的结果
Promise.race([
new Promise((resolve, reject) => {
let t = setTimeout(() => {
clearTimeout(t)
resolve('result A')
}, 1000);
}),
new Promise((resolve, reject) => {
let t = setTimeout(() => {
clearTimeout(t)
resolve('result B')
}, 100);
})
]).then(res => {
console.log(res)//result B
})
Promise 嵌套语法糖
来看两段代码:
代码一
let promise = new Promise((resolve, reject) => {
resolve(['第一个'])
}).then(res => {
res.push('第二个')
return new Promise((resolve, reject) => {
resolve(res)
})
}).then(res => {
res.push('第三个')
return new Promise((resolve, reject) => {
resolve(res)
})
}).then(res => {
console.log(res)
})
输出结果为:[‘第一个’, ‘第二个’, ‘第三个’]
代码二
let promise = new Promise((resolve, reject) => {
resolve(['第一个'])
}).then(res => {
res.push('第二个')
return res;
}).then(res => {
res.push('第三个')
return res;
}).then(res => {
console.log(res)
})
输出结果为:[‘第一个’, ‘第二个’, ‘第三个’]
以上两段代码是等价的,在Promise 嵌套中关于 new promise 部分的代码可以不用写,简洁非常多。但是代码的直观性上差了些,用不用这个语法糖就看个人喜好了