关于 js 的 promise

2021 年 11 月 23 日更新。最近突然意识到,Promise 是一个 Monad,其 then 方法兼具 fmap 和 bind 的功能——返回值为新的 Promise 对象,或是为普通的值,都能正确处理,所谓的 await,async 语法很像 Haskell 中的 do。比如下面两个 asyncFunc 就是等价的。

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
function fun1() {
return new Promise((resolve, reject)=>{
setTimeout(()=>{resolve(100)}, 1000)
})
}
function fun2() {
return new Promise((resolve, reject)=>{
setTimeout(()=>{resolve(100)}, 1000)
})
}

async function asyncFunc1() {
const numA = await fun1()
console.log("numA:" + numA)
const numB = await fun2()
console.log("numB:" + numA)
return numA + numB;
}

function asyncFunc2() {
return fun1().then(numA => {
console.log("numA:" + numA)
return fun2().then(numB => {
console.log("numB:" + numA)
return numA + numB;
})
})
}

promise 是 ES6 为解决异步编程过于复杂的问题所提出的一种技术/异步模式。对其的学习是必要的。

什么是 promise

promise,意为“承诺”,它归根结底是一个类,其将异步操作(或同步操作)封装在自己的构造器中并获取结果或错误。其后其允许使用异步方法取出结果。

promise 的优点在于其能避免回调地狱——多层的嵌套,将嵌套调用变成链式调用,且其保存的结果将被缓存以备无数次的监听。其也提供了合适的方式以方便错误和异常的处理。promise 当然也有缺点——不然也不会有像 RxJS 这样其它的异步解决方案了,但是这是我当前感觉不到的。

Promise 构造器和 then,catch

使用 Promise 的一个比较典型的方式是使用其构造函数,其构造函数接受一个参数——它称为执行器,executor,该执行器有两个参数——resolve 和 reject,其都是函数,其作用为收集结果或错误,用户将异步操作置于其函数体中,并通过 resolve 收集结果,或通过 reject 收集错误,如果遇到异常,则效果同 reject 方法相同。整个函数体将会同步执行

Promise 对象维护两个属性——其一是其状态(state),状态分为三种——pending,resolve(fulfilled),reject。pending 是默认状态,标识还未获得结果。在调用 resolve 或 reject 时,状态将发生转移——从 pending 到 resolve,从 pending 到 reject,这种状态转移只会执行一次,也就是说它只会收集第一次调用 resolve 或 reject 的结果,其后的调用将被忽视(但是函数的执行不会被中断);另一个属性则是结果。

then

用户可以使用 then 方法,通过其回调接受 promise 的结果,then 方法是可以无数次执行的,其回调是异步的。下面是一个示例。then 也可以接受第二个函数参数——代表 reject 时的回调。可以认为 then 是在这里注册了一个临时的监听器,在状态为 fulfilled 时执行函数体,因此它是异步的,即使状态早在 then 方法执行前改变也是如此。

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

// ts 能够通过泛型指定 resolve 函数接受的类型
const p = new Promise<string>((resolve, reject) => { // 同步
console.log("进入执行器函数体了")
setTimeout(()=>{
resolve("hello, happy world!")
console.log("resolve 了")
}, 1000)
})
console.log("退出执行器函数体了")
p.then((v)=>{ // 异步
console.log("获取到了")
console.log(v)
})
console.log("then 方法之后")

setTimeout(()=>{
p.then(v=>{ // 多次接受
console.log("能够多次获取,结束后能继续获取")
console.log(v)
}).catch(console.log) // 通过链式调用 catch 方法能够获取执行过程中抛出的异常

}, 2000)

/*
输出结果:

进入执行器函数体了
退出执行器函数体了
then 方法之后
resolve 了
获取到了
hello, happy world!
能够多次获取,结束后能继续获取
hello, happy world!
*/

then 方法的返回值也是 promise 对象(这或许是 promise 相比与原先的以回调为基础的异步编程的最大的差别吧?通过这种性质,then 方法能够对多个同步/异步任务进行串联,链式调用),其结果取决于 then 方法中函数的返回值(如果成功,调用 onfulfilled,如果失败,调用 onrejected)。函数正常返回时,返回一个结果为返回值的 resolved 的 promise 对象;函数抛出异常时,返回一个结果为异常的 rejected 的 promise 对象;函数返回新的 promise,则返回值就为该 promise。

1
2
3
4
5
6
7
8
9
// 测试 then 方法的返回值
const p = Promise.resolve(1)
let res = p.then((v)=>{
return 1000;
})
setTimeout(()=>{
// Promise { 1000 }
console.log(res) // 必须使用 setTimeout 或者 res.then,否则状态一定是 pending,这是因为其是异步的
}, 0)

利用 then 的这种特性,可以进行链式调用。

1
2
3
4
5
6
7
8
9
10
Promise.resolve()
.then(()=>{
console.log(1);
})
.then(()=>{
console.log(2);
})
.then(()=>{
console.log(3);
})

可以通过返回一个永远在 pending 状态的 promise 来中断调用链,这是中断的唯一办法(异常不能算中断,它进入了错误处理逻辑中)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Promise.resolve()
.then(()=>{
console.log(1);
return new Promise(()=>{}) // 不会再执行下去了,实践上来说这个应该放在条件语句中
})
.then(()=>{
console.log(2);
})
.then(()=>{
console.log(3);
})
/*
输出:

1
*/

进行链式调用时,必须要遵循这样的规范——同步调用直接写在方法的参数的函数体中,异步调用写在新的 promise 对象的执行器中。

catch 和异常穿透

catch 方法接受这样一个函数参数——它在 reject 时执行。乍得一看 catch 好似是 then 的子集,没有意义,但是 catch 有这样一个特性——异常穿透,当进行 then 的链式调用时,链条上抛出的异常/错误会直接到达 catch,并且错误提示中能够指出错误发生的具体位置!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Promise.resolve()
.then(()=>{
console.log(1);
})
.then(()=>{
throw new Error("err!")
console.log(2);
})
.then(()=>{
console.log(3);
})
.catch(err=>{console.warn(err)})
/*
输出结果:
1
Error: err!
at path/to/the/file.ts:19:15 居然能够给定发生异常的位置!nba
*/

一个简单的实现

下面展示了对 Promise 的一个简单的实现。其中 then 的实现是比较重要的——它将返回一个新的 Promise,在执行器中,它将等待直到当前 Promise 实例的状态不为 pending,然后修改自身的状态。这里可以有其它的实现方法——比如在 Promise 中维护一个保存 then 中回调的数组作为属性,调用 then 方法时,如果 Promise 仍旧是 pending 状态,则将这时的上下文添加到数组中,在状态改变时,即调用 resolved,reject 方法时,对数组进行遍历执行。如果不是 pending,则直接执行(但应当通过 setTimeout 将其变为异步)。同时关于 result 是 promise 时进行处理的逻辑也转移到 then 方法中执行。

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
// 不会用 ts 的泛型,随便写写
enum PromiseState {
PENDING, FULFILLED, REJECTED
}

// 工具函数,在 signal 函数返回真时执行回调,否则持续执行
function waitUntil(doSomething:()=>void,signal:()=>boolean) {
const interval = setInterval(()=>{ // setInterval 的性能可能是不好的
if (signal()) {
doSomething();
clearInterval(interval)
return;
}
})
}
class MyPromise<T> {
// 状态
private state : PromiseState = PromiseState.PENDING
// 值
private value : any = null

private setToPromise(anotherMyPromise : MyPromise<T>) {
waitUntil(()=>{
this.value = anotherMyPromise.value
this.state = anotherMyPromise.state
}, ()=>{
return anotherMyPromise.state !== PromiseState.PENDING;
})
return;
}
constructor(executor : (resolve : (arg0: T)=>void, reject : (arg0: any)=>void)=>void){
const resolve = (arg : T) => {
if (this.state !== PromiseState.PENDING) return; // 状态只改变一次
if (arg instanceof MyPromise) { // 如果参数就是 Promise
this.setToPromise(arg)
}
this.value = arg;
this.state = PromiseState.FULFILLED;
}
const reject = (arg : unknown) => {
if (this.state !== PromiseState.PENDING) return;
if (arg instanceof MyPromise) { // 如果参数就是 Promise
this.setToPromise(arg)
}
this.value = arg;
this.state = PromiseState.REJECTED;
}
try {
executor(resolve, reject)
}
catch(err) {
reject(err);
}
}

public then<V>(onfulfilled : (v : T) => V, onrejected? : (v : unknown) => unknown) : MyPromise<unknown> {
// 调用 then 时,相当于设置了一个临时的监听器。
return new MyPromise((resolve, reject) => {
waitUntil(()=>{
if (this.state === PromiseState.REJECTED) {
if (!onrejected) {
reject (this.value)
return
}
reject(onrejected(this.value))
}
else if (this.state === PromiseState.FULFILLED) {
try {
const res = onfulfilled(this.value)
resolve(res)
}
catch (err) { // 调用 onfulfiled 时发生异常
reject(err)
}
}
}, ()=>{
return this.state !== PromiseState.PENDING
})
})
}
public catch(onrejected : (v : unknown) => unknown) {
return new MyPromise((resolve, reject) => {
waitUntil(()=>{
if (this.state === PromiseState.REJECTED) {
if (!onrejected) {
reject (this.value)
return
}
reject(onrejected(this.value))
}
}, ()=>{
return this.state !== PromiseState.PENDING
})
})
}

public static resolve<V>(arg : V) : MyPromise<V> {
if (arg instanceof MyPromise) return arg;
return new MyPromise((resolve, reject) => {resolve(arg)})
}
}

#all,#race

all 和 race 是 Promise 提供的两个比较重要的静态方法。前者接受一个 promise 的数组并返回一个 promise 对象,其将等待所有 promise 都执行完毕且全为 resolved 时收集所有结果。race 方法也接受一个 promise 数组并返回一个 promise 对象,其将等待某 promise 最先执行完成并收集其结果,无论是 resolved 还是 reject。

async,await

async 和 await 是两个非常方便的操作符。async 将函数转换成返回 promise 的异步函数,结果的类型取决于 return 的结果,如果 return 一个非 promise 的值,其将被包装,如果 return 一个 promise,那就是 return 该 promise。抛出异常则返回一个 rejected 的 promise。

await 对 promise 进行操作,其将等待直到 promise 返回结果。await 的返回值为 promise 的结果。await 必须在 async 函数内部使用。通过 async 和 await,可以轻易将异步操作转换成同步操作流,对编写非常方便。

await 所接受的 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
// 其实就这里来说,这里的 async 是无关紧要的,可以去掉
const fn1 = async () => {
console.log("异步过程 1")
return new Promise((resolve, reject)=>{
setTimeout(()=>{
resolve(123)
}, 2000)
})
}

const fn2 = async () => {
console.log("异步过程 2")
return new Promise<number>((resolve, reject)=>{
setTimeout(()=>{
resolve(456)
reject("dsdsdssd")
}, 1000)
})
}
const mainFn = async () => {
const res1 = await fn1();
const res2 = await fn2();
console.log(res1, res2, "wow")
}
mainFn().then() // 经过一定的配置可以在顶层使用 await


本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 协议 ,转载请注明出处!