JavaScript里的Promise

  JavaScript在ES6版本中新增了Promise对象。

概述

Promise 对象是一个代理对象(代理一个值),被代理的值在Promise对象创建时可能是未知的。它允许你为异步操作的成功和失败分别绑定相应的处理方法(handlers)。

以上引用自MDN中对Promise的介绍。

  Promise其实就是一个对异步操作结果的封装。一个Promise有三种状态:

  • Pending: 初始状态,既不是成功,也不是失败状态。
  • Fulfilled: 表示操作成功完成。
  • rejected: 表示操作失败。

  Promise翻译为保证,承诺的意思。这意味着一个Promise一旦有了结果,就不会再发生改变。Pending是初始状态,此时表明还没有获取到结果。一旦状态从Pending改变为Fulfilledrejected后,状态便不会再发生改变。

  在之前的ES5时代,我们通常会用回调函数来处理异步操作的结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function successCallback() {
console.log('success!')
}
function failCallback() {
console.log('fail!')
}

function test() {
if (true) { // 异步请求的结果
successCallback()
} else {
failCallback()
}
}
test()

  上面的代码先是封装了两个回调函数,分别是成功和失败。然后在test函数内执行异步请求(忽略),根据异步请求的结果来执行对应的回调函数。如果在只有一层异步请求的时候,代码结构还算清晰。但实际业务中,往往会嵌套多层异步请求,层数一多,还用这种写法层层嵌套的话,代码会变得难以阅读和维护,也就是经典的回调地狱模式。所以这种情况就可以使用ES6新增的Promise方法。

  Promise对象使用new操作符创建,接收一个函数作为参数,该函数又接收resolvereject两个函数,这两个函数是ES6帮我们封装好的,我们直接调用就可以了。resolve表示成功时执行的函数,而reject表示失败时执行的函数。两个函数都可以传参,后期在.then里接收。

Promise.then()

  .then()方法用于Promise实例的状态改变时执行的回调函数。它接收两个参数,分别是状态成功时和状态失败时执行的回调函数,同时两个函数内可以传递参数供回调函数使用。

  下面我们来创建一个Promise实例并用.then()方法执行相应的回调函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const a = 3

function p(value) {
return new Promise((resolve, reject) => {
if (true) { // 如果异步操作成功
resolve(value)
console.log('success!')
} else { // 异步操作失败
reject(value)
console.log('fail!')
}
})
}

p(a).then((a) => {
console.log(a)
}, (a) => {
console.log(a)
})

  上面的代码我们首先声明了一个属性a,紧接着在p函数里封装了一个Promise对象并返回。里面假设进行了一个异步请求。如果异步请求执行成功,执行resolve函数,并把a当作参数传递进去。如果异步请求执行失败,执行reject函数,同时也把参数a传递进去。之后我们执行了p函数,将参数a传递进去,并在.then()内写了两个箭头函数用于执行成功和失败的回调。这时运行代码控制台会输出:

1
2
success!
3

  运行到这里说明走了resolve函数,并且获取到了传进来的参数a。如果此时将p函数内的异步操作结果修改为false,就会执行reject函数内的代码了。这里需要注意的是,success虽然写在resolve的后面,但是先于resolve执行的。因为在Promise对象里,同步代码总是先于异步代码执行。

  这就是Promise的用法。使用Promise来封装异步请求,结构清晰,写法简明,即使有多层嵌套也不会出现回调地狱的情况。.then()方法有一个链式调用的特点。一个Promise对象会返回一个新的Promise对象,所以如果都是Promise对象的话,可以这样用(前提是都用Promise包装起来)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 以下Promise对象省略了reject函数
const json = {
data: {
list: [1, 2, 3]
}
}
function p1(json) {
return new Promise((resolve) => {
setTimeout(() => {
console.log(json.data)
return resolve(json.data)
}, 1000)
})
}
function p2(data) {
return new Promise((resolve) => {
setTimeout(() => {
console.log(data.list)
resolve(data.list)
}, 1000)
})
}
function p3(list) {
return new Promise((resolve) => {
setTimeout(() => {
console.log(list[2])
resolve(list[2])
}, 1000)
})
}

p1(json).then(p2).then(p3)

/*
{list: [1, 2, 3]}
[1, 2, 3]
3
(每行输出间隔1s)
*/

  上面的代码首先模拟了一个后台请求到的数据,一个json对象,里面包含了data对象,data对象下是一个数组。接下来分别封装了p1, p2, p3三个Promise对象函数。每一层内用setTimeout定时器模拟了执行异步请求,间隔1秒。在定时器里把当前接收到的数据的下一层打印出来,同时以resolve函数参数的形式传递出去。最后执行.then()方法的时候,我们可以看到,每一层函数都打印出了上一层传递进来的数据。也就是说,上层Promise对象传递出来的属性,.then()方法可以接收到。

Promise.catch()

  .catch()方法可以捕获到Promise对象内的错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function p() {
return new Promise((resolve, reject) => {
if (false) { // 如果异步操作成功
resolve()
} else { // 异步操作失败
reject()
}
})
}

p()
.then(() => {
console.log('success!')
})
.catch(() => {
console.log('fail!')
})

  上面的代码用.catch()捕获了Promise对象内模拟的异步请求失败的场景,执行了reject()函数。我们也可以指定.then()方法的第二个参数,也就是执行状态失败时的回调函数来匹配错误的情况,但是实际使用中不推荐这么写,推荐使用.catch()方法来捕获错误状态,同时指定需要执行的函数。

Promise.finally()

  .finally()方法接收一个回调函数作为参数,该方法指定Promise对象的状态结束时执行的回调函数。也就是说,不管此时状态为成功还是失败,只要从Pending状态变为了其它两种状态的任意一种,都会执行.finally()方法里的回调函数。finally()方法的回调函数不接受任何参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function p() {
return new Promise((resolve, reject) => {
if (false) { // 如果异步操作成功
resolve()
} else { // 异步操作失败
reject()
}
})
}

p()
.then(() => {
console.log('success!')
})
.catch(() => {
console.log('fail!')
})
.finally(() => {
console.log('finally!')
})

  上面的代码使用了.finally()方法,无论Promise对象执行的是resolve还是reject,.finally()里的回调函数都会执行!

Promise.all()

  在实际使用中,有这样一种情况。我们想监听多个Promise对象的返回情况,如果所有的Promise对象都返回成功状态,才执行成功的回调。.all()方法就是用来干这个的,它接收一个数组参数,数组里的每一项都是一个Promise对象,都返回成功时.all()方法执行成功回调,如果有一项返回失败,则执行失败回调。这个方法的回调参数接收数组内每个Promise对象返回的参数,并按照顺序将它们放到一个数组里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const p1 = new Promise((resolve, reject) => {
resolve(4)
})
const p2 = new Promise((resolve, reject) => {
resolve(5)
})
const p3 = new Promise((resolve, reject) => {
resolve(6)
})

Promise.all([p1, p2, p3]).then((values) => {
console.log('success!')
console.log(values) // [4, 5, 6]
})

  上面的代码先创建里3个Promise对象(省略了模拟异步请求的步骤,直接执行了resolve方法),然后将这三个Promise对象以数组的形式传递给.all()方法,同时每个Promise对象里都分别传递了一个数字参数进去。.all()方法监听到三个Promise对象状态均为成功,则执行.then()方法内的代码,打印出了success!。同时三个Promise对象传递进来的数字参数也被接收到了。

  如果.all()方法的参数里有状态为失败的对象,则.all()方法则执行失败回调,同时接收到的参数为第一个执行reject函数时传递进来的参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const p1 = new Promise((resolve, reject) => {
resolve(4)
})
const p2 = new Promise((resolve, reject) => {
reject(5)
})
const p3 = new Promise((resolve, reject) => {
reject(6)
})

Promise.all([p1, p2, p3])
.then((value) => { // 这里不会被执行
console.log('success!')
console.log(value)
})
.catch((value) => { // 执行这里
console.log('fail!')
console.log(value) // 5
})

Promise.race()

  race译为赛跑,速度竞赛,顾名思义,这个方法就是返回最先改变状态的Promise实例。直接看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(4)
}, 1000)
})
const p2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(5)
}, 800)
})
const p3 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(6)
}, 900)
})

Promise.race([p1, p2, p3])
.then((value) => {
console.log(value) // 5
})

  上面代码还是首先封装了三个Promise对象,里面用setTimeout定时器模拟了异步请求,分别为1000ms,800ms和900ms,并分别传递了一个数字参数进去。可以看到最终是p2先执行完,所以打印出的是800ms的p2传递进去的参数5。

0%