Promise

Promise 是 ES6 新增的引用类型,可以通过 new 操作符来实例化。创建新 promise 时需要传入函数作为参数

promise 基础

promise 是一个有状态的对象,可以处于以下3种状态之一:

  • 待定(pending)
  • 兑现(fulfilled,有时也称为“解决”,resolved)
  • 拒绝(rejected)

promise 状态落定后不能再修改,即状态不可逆
promise 状态是私有的,不能直接通过 JavaScript 检测到,也不能被外部 JavaScript 代码修改

每个 promise 只要状态落定,就会有一个私有的内部值,该值包含原始值或对象的不可修改的引用。默认值为 undefined。

promise基础

控制 promise 状态的转换是通过调用它的两个函数参数实现的。这两个参数通常被命名为 resolve() 和 reject()。调用 resolve() 会把状态切换为兑现,调用 reject() 会把状态切换为拒绝,同时会抛出错误

为避免 promise 卡在待定状态,可以添加一个定时退出功能

1
2
3
4
let p = new Promise((resolve, reject)=>{
setTimeout(reject, 10000)
// do something
})

因为 promise 的状态只能改变一次,所以如果这里的代码在超时前已经解决或者拒绝,那么超时回调再尝试拒绝也会静默失败

axios 的请求超时时间设置是否用了这个?

promise 的实例方法

promise 实例的方法是连接外部同步代码与内部异步代码之间的桥梁。这些方法可以访问异步操作返回的数据,处理 promise 成功和失败的结果,连续对 promise 求值,或者添加只有 promise 进入终止状态时才会执行的代码

Promise.prototype.then()

Promise.prototype.then() 是为 Promise 实例添加处理程序的主要方法

then() 方法接收最多两个参数:onResolved 处理程序和 onRejected 处理程序。这两个参数都是可选的,如果提供的话,会在进入“兑现”和”拒绝”状态时执行

如果想只提供 onRejected 参数,就要在 onResolved 参数的位置上传入 null,这样有助于避免在内存中创建多余的对象

1
2
let p1 = new Promise((resolve, reject) => setTimeout(resolve, 1000))
p1.then(null, onRejected('p2'))

Promise.prototype.catch()

Promise.prototype.catch() 方法用于给 Promise 添加拒绝处理程序

这个方法只接收 onRejected 处理程序。事实上,这个方法就像一个语法糖,调用它就相当于调用 Promise.prototype.then(null, onRejected)

1
2
3
4
5
6
7
8
let p = Promise.reject()
let onRejected = function(e){
setTimeout(console.log, 0, 'rejected')
}

// 以下两种添加拒绝处理程序的方式是一样的
p.then(null, onRejected) // rejected
p.catch(onRejected) // rejected

Promise.prototype.finally()

Promise.prototype.finally() 方法用于给 Promise 添加 onFinally 处理程序

这个处理程序在 Promise 转换为解决或拒绝状态时都会执行。这个方法可以避免 onResolved 和 onRejected 处理程序中出现冗余代码。但 onFinally 处理程序没有办法知道 Promise 的状态是解决还是拒绝,所以这个方法主要用于添加清理代码

Promise.prototype.finally() 方法返回一个新的 Promise 实例

1
2
3
4
5
let p1 = new Promise(() => {})
let p2 = p1.finally()
setTimeout(console.log, 0, p1) // Promise <pending>
setTimeout(console.log, 0, p2) // Promise <pending>
setTimeout(console.log, 0, p1 === p2) // false

返回的新实例不同于 then() 或 catch() 方式返回的实例。因为 onFinally 被设计为一个状态无关的方法,所以在大多数情况下它将表现为父 Promise 的传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let p1 = Promise.resolve('foo')

let p2 = p1.finally()
let p3 = p1.finally(() => undefined)
let p4 = p1.finally(() => {})
let p5 = p1.finally(() => Promise.resolve())
let p6 = p1.finally(() => 'bar')
let p7 = p1.finally(() => Promise.resolve(''bar))
let p8 = p1.finally(() => Error('qux'))

setTimeout(console.log, 0, p2) // Promise <resolved>: foo
setTimeout(console.log, 0, p3) // Promise <resolved>: foo
setTimeout(console.log, 0, p4) // Promise <resolved>: foo
setTimeout(console.log, 0, p5) // Promise <resolved>: foo
setTimeout(console.log, 0, p6) // Promise <resolved>: foo
setTimeout(console.log, 0, p7) // Promise <resolved>: foo
setTimeout(console.log, 0, p8) // Promise <resolved>: foo

如果返回的是一个待定状态,则会返回相应的实例状态

1
2
3
let p9 = p1.finally(() => new Promise(() => {}))

setTimeout(console.log, 0, p9) // Promise <pending>

非重入 Promise 方法

当 Promise 进入落定状态时,与该状态相关的处理程序会被排期,而非立即执行。跟在这个处理程序的代码之后的同步代码一定会在处理程序之前先执行。即使 Promise 一开始就是与附加处理程序关联的状态,执行顺序也是这样的。这个特性由 JavaScript 运行时保证,被称为“非重入”(non-reentrancy)特性

1
2
3
4
5
6
7
8
9
10
11
12
// 创建解决的 promise
let p = Promise.resolve()

// 添加解决处理程序
p.then(() => console.log('resolved handler'))

// 同步输出,证明 then() 已经返回
console.log('then() returns')

// 实际打印结果
then() returns
resolved handler

先添加处理程序后解决 Promise 也是一样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let syncResolve

// 创建 Promise 实例并将解决函数保存在一个局部变量中
let p = new Promise((resolve) => {
syncResolve = function() {
console.log('first')
resolve()
console.log('second')
}
})

p.then(() => console.log('fourth'))

syncResolce()

console.log('thirth')

// 实际输出
first
second
thirth
fourth

非重入适用于 onResolved/onRejected 处理程序、catch() 处理程序和 finally() 处理程序

Promise 连锁与合成

连锁

每个 Promise 实例的方法(then()、catch() 和 finally())都会返回一个新的 Promise 对象,而这个新 Promise 对象又有自己的实例方法,这样连缀方法调用就可以构成所谓的“连锁”

1
2
3
4
5
6
7
8
9
10
11
12
let p = new Promise((resolve, reject) => {
console.log('first')
resolve()
})
p.then(() => console.log('second'))
.then(() => console.log('third'))
.then(() => console.log('fourth'))

// first
// second
// third
// fourth

上面的代码实现了一连串同步任务,要真正执行异步任务,可以让每个执行器都返回一个 Promise 实例,这样就可以让每个后续的 Promise 都等待之前的结果,也就是串行化异步任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let p1 = new Promise((resolve, reject) => {
console.log('p1')
setTimeout(resolve, 1000)
})

p1.then(() => new Promise((resolve, rejcet) => {
console.log('p2')
setTimeout(resolve, 1000)
}))
.then(() => new Promise((resolve, rejcet) => {
console.log('p3')
setTimeout(resolve, 1000)
}))
.then(() => new Promise((resolve, rejcet) => {
console.log('p4')
setTimeout(resolve, 1000)
}))

// p1(1秒后)
// p2(2秒后)
// p3(3秒后)
// p4(4秒后)

把以上代码提取到一个工厂函数中,就可以写成这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function delayedResolve(str){
return new Promise((resolve, reject) => {
console.log(str)
setTimeout(resolve, 1000)
})
}

delayedResolve('p1')
.then(() => delayedResolve('p2'))
.then(() => delayedResolve('p3'))
.then(() => delayedResolve('p4'))

// p1(1秒后)
// p2(2秒后)
// p3(3秒后)
// p4(4秒后)

Promise.all()

Promise.all() 静态方法创建的 Promise 会在一组 Promise 全部解决之后再解决。这个静态方法接受一个可迭代对象,返回一个新 Promise 实例

如果至少有一个包含的 Promise 状态为待定,则合成的 Promise 也会待定;如果有一个包含的 Promise 拒绝,则合成的 Promise 也会拒绝

如果所有 Promise 都成功解决,则合成 Promise 的解决值就是所有包含 Promise 解决值的数组,按照迭代器顺序

1
2
3
4
5
6
7
let p = Promise.all([
Promise.resolve(3),
Promise.resolve(),
Promise.resolve(4)
])

p.then((values) => setTimeout(console.log, 0, values)) // [3, undefined, 4]

如果有 Promise 拒绝,则第一个拒绝的 Promise 会将自己的理由作为合成 Promise 的拒绝理由。之后再拒绝的 Promise 不会影响最终 Promise 的拒绝理由。不过,这并不影响所有包含 Promise 正常的拒绝操作。合成的 Promise 会静默处理所有包含 Promise 的拒绝操作

1
2
3
4
5
6
7
8
// 虽然只有第一个 Promise 的拒绝理由会进入拒绝处理程序,第二个 Promise 的拒绝也会被静默处理,不会有错误跑掉
let p = Promise.all([
Promise.rejcet(3),
new Promise((resolve, reject) => setTimeout(reject, 1000))
])

p.catch((reason) => setTimeout(console.log, 0, reason)) // 3
// 没有未处理的错误

Promise.race()

Promise.race() 静态方法返回一个包装 Promise,是一组集合中最先解决或拒绝的 Promise 的镜像。这个方法接收一个可迭代对象,返回一个新 Promise

Promise.race() 不会对解决或拒绝的 Promise 区别对待。无论是解决还是拒绝,只要是第一个落定的,Promise.race() 就会包装其解决值或拒绝理由并返回新 Promise

串行 Promise 合成

基于后续 Promise 使用之前 Promise 的返回值来串联 Promise 是 Promise 的基本功能。这很像函数合成,即将多个函数合称为一个函数,比如:

1
2
3
4
5
6
7
8
9
function addTwo(x) { return x + 2 }
function addThree(x) { return x + 3 }
function addFive(x) { return x + 5 }

function addTen(x) {
return addFive(addTwo(addThree(x)))
}

console.log(addTen(7)) // 17

在上面的例子中,3个函数基于一个值合成为一个函数。类似地,Promise 也可以像这样合成起来,渐进地消费一个值,并返回一个结果:

1
2
3
4
5
6
7
8
function addTen(x) {
return Promise.resolve(x)
.then(addTwo)
.then(addThree)
.then(addFive)
}

addTen(8).then(console.log) // 18

使用 Array.prototype.reduce() 可以写成更简洁的形式:

1
2
3
4
5
function addTen(x) {
return [addTwo, addThree, addFive].reduce((promise, fn) => promise.then(fn), Promise.resolve(x))
}

addTen(8).then(console.log) // 18

这种模式可以提炼出一个通用函数,可以把任意多个函数作为处理程序合成一个连续传值的 Promise 连锁。这个通用的合成函数可以这样实现:

1
2
3
4
5
6
7
function compose(...fns) {
return (x) => fns.reduce((promise, fn) => promise.then(fn), Promise.resolve(x))
}

let addTen = compose(addTwo, addThree, addFive)

addTen(8).then(console.log) // 18

异步函数

异步函数,也称为“async/await”(语法关键字),是 ES8 新增规范。这个特性从行为和语法上都增强了 JavaScript,让以同步方式写的代码能够异步执行