JavaScript - 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
new Promise((resolve, reject) => {
console.log("外部promise");
resolve();
})
.then(() => {
console.log("外部第一个then");
new Promise((resolve, reject) => {
console.log("内部promise");
resolve();
})
.then(() => {
console.log("内部第一个then");
})
.then(() => {
console.log("内部第二个then");
});
return new Promise((resolve, reject) => {
console.log("内部promise2");
resolve();
})
.then(() => {
console.log("内部第一个then2");
})
.then(() => {
console.log("内部第二个then2");
});
})
.then(() => {
console.log("外部第二个then");
});

Are you ok ? 恭喜你被谷歌录取了 : 行了行了下一位

Why

首先我们先来了解一下引入 Promise 的必要性。

回调地狱

曾几何时(现在也是),我们的异步代码是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
post('/user/login', function (err, data) {
if (err) return err;
get('/user/info', function (err, data) {
if (err) return err;
post('/articles/create', function (err, data) {
if (err) return err;
get('/articles/list', function (err, data) {
...
});
});
});
});

这就是大家津津乐道的 回调地狱 了,主要有以下几个问题:

  1. 嵌套太深,逻辑复杂:每层的回调函数的业务逻辑都依赖于上层执行的返回结果,嵌套层次多了之后,代码可读性很差。
  2. 错误处理麻烦:每层的回调函数都需要传入两个状态(失败、成功),且每一层都需要对错误进行单独处理,没有统一的错误处理机制。
  3. 上下文环境乱:有的时候我们想处理上层环境(调用 this),却发现绑定不到,只能使用 var _this = this; 这样的 hack 方法。
Promise

这时候天空一声巨响,Promise 闪亮登场,救人民与水深火热之中。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//用 Promise 封装接口
const post = (url) => {
return new Promise((resolve, reject) => {
post(url, (err, data) => {
err && reject(err)
resolve(data)
})
})
}

//调用逻辑
post('/user/login')
.then(data => get('/user/info'))
.then(data => post('/articles/create'))
.then(data => get('/articles/list'))
.then(...)
.catch(err => console.error('报错了啦:' + err))

这不是好起来了嘛:

  1. 干掉了嵌套调用,采用了链式挂载,逻辑上清晰了一些。
  2. 合并处理错误,在最后统一 catch 并执行。
async / await

Emmm,还不是一坨代码。好吧,再来看看终极方案 async/await

1
2
3
4
5
6
7
8
9
10
11
const do = async () => {
try {
const token = await post('/user/login');
const userInfo = await get('/user/info');
const createArticleResult = await post('/articles/create');
const articlesList = await get('/articles/list');
...
} catch (err) {
console.error('又报错了啦:' + err)
}
}

直接屏蔽了异步逻辑,改用同步方式。

wo

API

Promise - MDN

工欲善其事,必先利其器。

原理

那么 Promise 是如何工作的呢?主要是采用了 延迟绑定返值穿透 两种思想。

延迟绑定
1
2
3
4
5
const p = new Promise((resolve, reject) => { resolve('ok') })

...

p.then((value) => { console.log(value) })

可以看到先创建了应该 Promise 对象,此时还没有绑定 回调函数。创建完对象之后,可以处理其他代码逻辑。直到调用 then 处理执行逻辑的时候,我们才将 回调函数 进行绑定。创建对象绑定回调 实现了分离解耦。

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

const p2 = p1.then((value) => {
const p2 = new Promise((resolve, reject) => {
resolve(value + 1)
})
return p2
})

p2.then((value) => {
console.log(value)
})

可以看到,在 p1 的回调函数中我们创建了一个新的 Promise 对象 p2,将它 return 到了最外层并被接受。每层回调函数的返回值始终可以穿透到最外层,这样就可以保证 返回值 始终可控,不会陷入循环中。

源码

上代码啊

其实经典方法的实现原理都大同小异:状态流传递、队列缓存回调……

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
const PENDING = 'PENDING'
const FULFILLED = 'FULFILLED'
const REJECTED = 'REJECTED'

class MyPromise {
constructor(handle) {
this._status = PENDING
this._value = undefined
//回调函数队列
this._fulfilledQueues = []
this._rejectedQueues = []
//执行handle
try {
handle(this._resolve.bind(this), this._reject.bind(this))
} catch (err) {
this._reject(err)
}
}

// resovle时执行的函数
_resolve(val) {
if (this._status !== PENDING) return
this._status = FULFILLED
this._value = val
}

// reject时执行的函数
_reject(err) {
if (this._status !== PENDING) return
this._status = REJECTED
this._value = err
}

// then方法
then(onFulfilled, onRejected) {
const { _value, _status } = this
return new MyPromise((onFulfilledNext, onRejectedNext) => {
//成功时执行的函数
let fulfilled = value => {
try {
let res = onFulfilled(value);
if (res instanceof MyPromise) {
// 如果当前回调函数返回Promise对象,必须等待其状态改变后在执行下一个回调
res.then(onFulfilledNext, onRejectedNext)
} else {
//否则会将返回结果直接作为参数,传入下一个then的回调函数,并立即执行下一个then的回调函数
onFulfilledNext(res)
}
} catch (err) {
onRejectedNext(err)
}
}
//失败时执行的函数
let rejected = error => {
try {
let res = onRejected(error);
if (res instanceof MyPromise) {
// 如果当前回调函数返回Promise对象,必须等待其状态改变后在执行下一个回调
res.then(onFulfilledNext, onRejectedNext)
} else {
//否则会将返回结果直接作为参数,传入下一个then的回调函数,并立即执行下一个then的回调函数
onFulfilledNext(res)
}
} catch (err) {
onRejectedNext(err)
}
}
switch (_status) {
//状态为pending时将回调函数推入队列进行缓存
case PENDING:
this._fulfilledQueues.push(fulfilled)
this._rejectedQueues.push(rejected)
break
//状态改变时执行回调函数
case FULFILLED:
fulfilled(_value)
break
case REJECTED:
rejected(_value)
break
}
})
}
}

参考资料

大厂面试必考知识点:Promise 注册微任务和执行过程

查看评论