JavaScript 里的 Promise

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

概述

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

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

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

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

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

  在之前的 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 操作符创建,接收一个函数作为参数,该函数又接收 resolve 和 reject 两个函数,这两个函数是 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。