Abo

小抄:手把手带你写原生Promise

提到异步编程,就会谈到这个家伙:Promise

论如何实现then对值的穿透性

前言

Promise 的本质是一个有限状态机,存在三种状态:

  • PENDING(等待)
  • FULFILLED(成功)
  • REJECTED(失败)

故手写 Promise第一步即是const创建状态变量

对于 Promise 而言,状态的改变不可逆,即由等待态变为其他的状态后,就无法再改变了

  • Promise.resolve():返回一个成功的Promise实例
  • Promise.reject():返回一个失败的Promise实例
  • Promise.finally():无论成功还是失败,都会执行
  • Promise.all():全部都成功就返回成功,有一个失败就结束返回失败
  • Promise.race():只要有一个结束了就结束,返回状态就是那一个的状态
  • Promise.any():只要有一个成功就返回成功,所有失败返回失败
  • Promise.allSettled():全部Promise都结束了,才结束,永远是成功状态

在待会手写的Promise中,将会囊括上述几种Promise方法,所以我们需要先对每个方法的功能及用法有个了解

  1. Promise.resolve的对象是Promise,那么这个Promise作为返回值返回。而且返回的Promise状态变为resolved

    1
    2
    3
    4
    5
    6
    function fn(resolve, reject){
    setTimeout(() =>{ resolve(123) },3000)
    }
    let p0 = new Promise(fn)
    let p1 = Promise.resolve(p0)
    console.log(p0 === p1) //true,返回的Promise即是传入的Promise对象

    在手写过程中,对于resolve方法的实现,我们只需要返回一个新的promise对象,当然是我们自定义的promise

  2. then返回的也是Promise实例,catch也是

    1
    2
    3
    let then0 = p0.then();
    let p2 = Promise.resolve(then0)
    console.log(then0 === p2) //true,then返回的是Promise实例

    then的实现会是手写promise的一大难点,需要考虑两个大问题

    首先对于参数的处理,默认参数当然是有成功回调与失败回调,需要考虑当用户不传参数的情况,需要做一些处理,直接返回函数值,用箭头函数来描述即:onFulfilled = val => val, onRejected = err => err

    第二个需要考虑的问题就是对于值的穿透性,容易忽略的一点就是我们每次调用then都需要返回一个新的promise对象

  3. 如果遇到reject,直接进入catch

    catch函数就相当于我们的Rejected状态,相当于我们去调用了一个只包含OnRejected的then函数,格式大概为then(null,OnRejected)

  4. 如果catch中有resolve,会进入之后的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
    function start() {
    return new Promise((resolve, reject) => {
    resolve(1);
    });
    }
    start()
    .then(data => {
    // promise p1
    console.log('result of p1: ', data);
    return Promise.reject(2); // p2
    })
    .then(data => {
    // promise p2
    console.log('result of p2: ', data);
    return Promise.resolve(3); // p3
    })
    .catch(ex => {
    // promise p3
    console.log('error: ', ex);
    return Promise.resolve(4); // p4
    })
    .then(data => {
    // promise p4
    console.log('result of p4: ', data);
    });

    // 输出结果
    // result of p1: 1
    // error: 2
    // result of p4: 4

    catch之后还是可以接then,可以理解成catch的本质就是一个特殊的then

温故知新

Promise如何消灭回调地狱

  1. 什么是回调地狱
    • 多层嵌套函数的问题
    • 每种任务的处理结果都存在成功或失败,需要在任务结束后分别处理这两种可能
  2. 解决方法
    • 回调函数延迟绑定:当任务结束后,去then或者catch中执行回调函数
    • 返回值穿透:通过then().then()…不断传递,和延迟绑定配合完成链式调用
    • 错误冒泡:错误会一直传递,被catch接收到(不管是在任务中还是在then中),不用频繁检查错误(见上面catch例子)
  3. 解决的效果
    • 实现链式调用,解决多层嵌套
    • 实现错误冒泡后一站式处理,防止增加代码混乱度

Promise为何要引进微任务(抽象)

Promise的执行函数是同步执行的(传入的函数立即执行),但是里面有异步操作,在异步操作结束后会调用 resolve方法,或者中途遇到错误调用 reject方法,这两者都是作为微任务进入到 EventLoop 中。

微任务中process.nextTick比then先执行

利用微任务解决了两大痛点:

  1. 使用异步回调替代同步回调解决了浪费CPU性能的问题
  2. 放到当前宏任务最后执行,解决了回调执行的实时性问题

Promise.all的顺利执行

Promise.all如果有一个 promise 抛出错误就不能执行then,可以通过在每个promise 中设置 try-catch,当异步语句出错进入catch时强行resolve,这样就可以执行Promise.all的then方法了。

all方法是全员通过才可,我们可以通过新建结果数组,保存 resolve值,当resolve的长度等于promises数组的长度,则可以顺利执行,相对而言race方法只要有一员通过即可

原生实现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
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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

function myPromise(fn){
let self = this
self.state = PENDING //默认内部状态
self.successVal = null
self.errorVal = null
self.onFulfilledCb = [] //调用resolve时拿出里面的方法
self.onRejectedCb = [] //调用reject时拿出里面的方法
//定义resolve为正常执行后的结果
const resolve = (res) =>{
if(self.state === PENDING){
//用setTimeout模拟异步调用
setTimeout(() =>{
self.state = FULFILLED
self.successVal = res
self.onFulfilledCb.forEach(func =>{
func(self.successVal)
})
}, 0)
}
}
//定义reject为异常处理后的结果
const reject = (err) =>{
if(self.state === PENDING){
setTimeout(() =>{
self.state = REJECTED
self.errorVal = err
self.onRejectedCb.forEach(func =>{
func(self.errorVal)
})
}, 0)
}
}

//定义完之后开始执行函数,要注意用try-catch捕获错误
try{
//将resolve和reject传给fn,由fn去调用他们返回结果
fn(resolve, reject)
}
catch(err){
reject(err)
}
}

myPromise.prototype.then = function(onFulfilled, onRejected){
//如果then两个参数不传,需要做一些处理
//成功回调返回promise的结果
//失败回调直接抛出错误
if(typeof onFulfilled !== 'function'){
onFulfilled = function(value){ return value }
}
if(typeof onRejected !== 'function'){
onRejected = function(error){ throw error }
}

let self = this

const nextPromise = new myPromise((resolve, reject) =>{
//链式操作前面的操作已经结束,直接执行结果并返回
if(self.state === FULFILLED){
setTimeout(() =>{
try{
//用传入的方法处理后返回
//状态变为成功,此时有相应的self.successVal
let res = onFulfilled(self.successVal)
resolve(res)
}
catch(err){
reject(err)
}
}, 0)
}
else if(self.state === REJECTED){
setTimeout(() =>{
try(){
//状态变为失败,此时有相应的self.errorVal
const res = onRejected(self.errorVal)
resolve(res)
}
catch(err){
reject(err)
}
}, 0)
}
else if(self.state === PENDING){
//链式操作前面还没结束,先塞进回调函数队列等待执行
self.onFulfilledCb.push(() =>{
setTimeout(() =>{
try{
const res = onFulfilled(this.successVal)
resolve(res)
}
catch(err){
reject(err)
}
}, 0)
})
self.onRejectedCb.push(() =>{
setTimeout(() =>{
try{
const res = onFulfilled(this.errorVal)
resolve(res)
}
catch(err){
reject(err)
}
}, 0)
})
}
})
//then返回的结果也是promise
return nextPromise
}

myPromise.prototype.catch = function (onRejected){
return this.then(null, onRejected);
}

myPromise.prototype.finally = function(fn){
return this.then(
successVal => { fn(); return successVal},
errorVal => { fn(); throw errorVal }
)
}

myPromise.resolve = function(successVal){
return new MyPromise((resolve, reject) =>{
resolve(successVal);
})
}

myPromise.reject = function(errorVal){
return new MyPromise((resolve, reject) =>{
reject(errorVal)
})
}

myPromise.all = function(promises){
return new Promise((resolve, reject) =>{
if (promises.length === 0){
resolve([]);
}
else{
let result = [];
let index = 0;
for (let i = 0; i < promises.length; i++){
promises[i].then(
successVal => {
//先存进结果数组
result[i] = successVal
//都放满了,再一次性返回
if (++index === promises.length) { resolve(result); }
},
errorVal => {
//有一个报错,就结束
reject(errorVal)
return
}
)
}
}
})
}

myPromise.race = function(promises){
return new Promise((resolve, reject) =>{
if(promises.length === 0){
resolve()
}
else{
let index = 0
for (let i = 0; i < promises.length; i++){
promises[i].then(
successVal =>{
resolve(successVal)
},
errorVal => {
//有一个报错就结束
reject(errorVal)
return
}
)
}
}
})
}

注意点:

  1. 保存回调函数,对应在PENDING状态下调用的then函数,且在保存回调函数的时候需要用setTimeout模拟异步,换言之,在push的时候就要包装要setTimeout来模拟

  2. 用setTimeout模拟异步

  3. 在then里面,对于REJECTED的状态,我们利用OnRejected拿到值之后,之后要调用resolve(res),拿到了失败的回调结果之后应该继续往下走,我们只有在catch到错误的时候才会去reject(err),待考究

  4. all跟race方法对于错误是一致的,有一个报错就结束。