ES6 Promise总结并实现一个Promise

深入理解Promise

Posted by xiaopf on 2019-12-17
Words 4.5k and Reading Time 20 Minutes
Viewed Times

ES6 Promise 总结并实现一个Promise

Promise使用总结

Promise 的含义

Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。

Promise 就是一个状态机

三种状态

  • pending(进行中)
  • fulfilled(已成功)
  • rejected(已失败)

状态转变

  • 初始化处于 pending 状态
  • 调用 resolve() 之后,pending -> fulfilled
  • 调用 reject() 之后,pending -> rejected

Promise的特点:

  • 对象的状态不受外界影响
  • 一旦状态改变,就不会再变

基本用法

Promise是一个构造函数,所以需要用new实例化返回一个promise的对象,可以直接在后面调用then,也可以在promise对象身上调用then。

Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。

1
const promise = new Promise((resolve, reject) => {
2
    //实例化的同时就已经触发执行了
3
    console.log(/*最早执行*/)
4
    //模拟异步操作
5
    setTimeout(() => {
6
        if (/*true*/) {
7
            resolve(/*给then回调传参*/)
8
        } else {
9
            reject(/*给catch回调传参*/)
10
        }
11
    }, 2000)
12
}).then((/*resolve给的参数*/) => {
13
    console.log(/*resolve给的参数*/)
14
}).catch((/*给catch回调传参*/) => {
15
    console.log(/*给catch回调传参*/)
16
})

Promise实例化接受的函数内部,是在实例化的同时就已经执行啦。

then方法第二个参数是可选的,是reject()执行后触发执行的回调函数,可以省略,直接用catch代替。

resolve函数接受的参数是会传递给then方法里面的回调函数作为参数的。catch可以捕获链式调用前面的错误,也可以获取reject传的的参数。

Promise.prototype.then()

Promise 实例具有then方法,也就是说,then方法是定义在原型对象,then方法返回的是一个Promise的实例,不是原来的Promise实例,而是一个全新的实例。因此then方法后面还可以继续调用then方法。前一个then接收的回调函数return的返回值,直接传递到下一个then接收的回调函数作为参数。如果前一个回调中返回的是一个promise对象,那么后一个then需要等到返回的promise变化后才会执行。

1
new Promise((resolve, reject) => {
2
    setTimeout(() => {
3
        resove(1)
4
    },1000)
5
}).then((value) => {
6
    console.log(value) // 1
7
    return 2
8
}).then((value) => {
9
    console.log(value) // 2
10
})

Promise.all()

Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。

1
const p = Promise.all([p1, p2, p3]).then((values)=>{
2
    console.log(values); //values是p1,p2,p3 resolve接受的参数的数组,如果某一个实例没有传参,则为数组对应的位置是undefind
3
}).catch((err)=>{
4
    console.log(err); // err是p1,p2,p3中第一个reject接受的参数,不是数组
5
})

接受一个数组为参数,数组中元素都是Promise的实例(如果不是,会调用Promise.resolve方法)。只有当所有Promise实例全部转变成 fulfilled 状态后,p才会变成fulfilled状态,只要有一个实例rejected,p的状态就变成rejected状态。

注意,如果作为参数的 Promise 实例,自己定义了catch方法,那么它一旦被rejected,并不会触发Promise.all()的catch方法。

Promise.race()

Promise.race()方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。

1
const p = Promise.race([p1, p2, p3]).then((value)=>{
2
    console.log(value); //value是p1,p2,p3中第一个resolve接受的参数,不是数组
3
}).catch((errs)=>{
4
    console.log(errs); // errs是p1,p2,p3 reject接受的参数的数组,如果某一个实例没有传参,则为数组对应的位置是undefind
5
})

接受一个数组为参数,数组中元素都是Promise的实例(如果不是,会调用Promise.resolve方法)。只要其中一个Promise实例转变成 fulfilled 状态,p 就会变成fulfilled状态,只有全部实例都rejected,p的状态才会变成rejected状态。

Promise.allSettled()

Promise.allSettled()方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。只有等到所有这些参数实例都返回结果,不管是fulfilled还是rejected,包装实例才会结束。该方法由 ES2020 引入。

1
const resolved = Promise.resolve(42);
2
const rejected = Promise.reject(-1);
3
4
const allSettledPromise = Promise.allSettled([resolved, rejected]);
5
6
allSettledPromise.then(function (results) {
7
  console.log(results);
8
});
9
// [
10
//    { status: 'fulfilled', value: 42 },
11
//    { status: 'rejected', reason: -1 }
12
// ]

有时候,我们不关心异步操作的结果,只关心这些操作有没有结束。这时,Promise.allSettled()方法就很有用。如果没有这个方法,想要确保所有操作都结束,就很麻烦。Promise.all()方法无法做到这一点。

Promise.resolve()

有时需要将现有对象转为 Promise 对象,Promise.resolve()方法就起到这个作用。

1
Promise.resolve('foo')
2
// 等价于
3
new Promise(resolve => resolve('foo'))

参数

  • 参数是 Promise 实例,那么Promise.resolve将不做任何修改、原封不动地返回这个实例。
  • 参数是一个thenable对象,则将这个对象转为 Promise 对象,然后就立即执行thenable对象的then方法。
  • 参数是一个原始值,或者是一个不具有then方法的对象,则Promise.resolve方法返回一个新的 Promise 对象,状态为resolved。
  • 调用时不带参数,直接返回一个resolved状态的 Promise 对象。

Promise.reject()

Promise.reject(reason)方法也会返回一个新的 Promise 实例,该实例的状态为rejected。

1
const p = Promise.reject('出错了');
2
// 等同于
3
const p = new Promise((resolve, reject) => reject('出错了'))
4
5
p.then(null, function (s) {
6
  console.log(s)
7
});
8
// 出错了

实现一个Promise

Promise的构造函数

  1. 由于后面会多次涉及到this实例对象,为了避免混乱直接用self拿到this代表的实例对象。
  2. 由于promise实例或涉及到3个状态,所以需要有一个status属性保存状态。
  3. resolve/reject会接受参数并传递给then接受的回调函数中做参数,所以需要一个data属性存放这个值。
  4. 当Promise实例状态转变之后,会执行then中的回调函数,所以需要两个保存回调的数组,至于为什么是数组,后面会举例说明。
  5. 当Promise实例化之后,构造函数收到回调函数会立即执行,因此需要立即执行fn函数,由于可能会出现报错(throw Error)所以需要用到 try catch
  6. 构造函数传入的回调函数在执行的时候需要 resolve 和 reject 两个函数作为参数,因此这两个函数也是在构造函数中定义的。
1
function PromiseX (fn) {
2
3
    // 避免后面this混乱
4
    const self = this;
5
6
    // 保存Promise实例状态
7
    self.status = 'pending';
8
9
    // 保存传递的参数数据
10
    self.data = '';
11
12
    // Promise resolve/reject时的回调函数集,因为在Promise结束之前有可能有多个回调添加到它上面
13
    // 非链式调用会出现多个,具体后面举例
14
    self.onResolvedQueue = [];
15
    self.onRejectedQueue = [];
16
17
    // 实例化立即执行fn
18
    try{
19
        fn(resolve,reject)
20
    } catch (e) {
21
        reject(e)
22
    }
23
24
    function resolve () {
25
        ....
26
    }
27
    function reject () {
28
        ....
29
    }
30
}

resolve reject 函数实现

resolve和reject实际上是在 Promise 接收到 fn 回调函数中执行的。

一般情况下是 fn 中的异步操作结束返回结果后,成功了 resolve(value),失败了 reject(reason)。

1
new Promise((resolve, reject) => {
2
    console.log(111)
3
    // 模拟异步操作
4
    setTimeout(() => {
5
        resolve(222)
6
    }, 2000)
7
}).then((value) => {
8
    console.log(value)
9
})

也有的情况不是异步操作,直接就调用了。

1
new Promise((resolve, reject) => {
2
    console.log(111)
3
    resolve(222)
4
}).then((value) => {
5
    console.log(value)
6
})

总之是Promise实例状态需要转变的时候就会调用他们了。

  1. 当状态要变化的时候,实际还没有变化,所以状态应该还是 pending 状态
  2. 将self.status转移到对应的状态。
  3. 需要把接收到的参数给self.data存起来.
  4. 状态转移完了,参数也保存完了,就需要执行then中的回调函数了
1
function PromiseX (fn) {
2
    //....
3
4
    function resolve (value) {
5
        if (selft.status === 'pending') {
6
            self.status = 'fulfilled';
7
            self.data = value;
8
            self.onResolvedQueue.forEach(resolved => resolved(value));
9
        }
10
    }
11
    function reject (reason) {
12
        if (selft.status === 'pending') {
13
            self.status = 'rejected';
14
            self.data = reason;
15
            self.onRejectedQueue.forEach(rejected => rejected(reason));
16
        }
17
    }
18
}

then 方法

Promise实例有一个then方法,是状态转变后的回调,很明显是应该写在原型上。具体使用上面有介绍。

  1. then方法调用后,立即就会进入执行阶段。
  2. then方法接受两个回调函数:一个是resolve后执行的,一个是reject后执行的,第二个可以省略。所以先判断两个是否是 function。
  3. 如果构造函数实例化的时候接受的回调函数里面有异步操作的话,then执行是先于状态转变的,也就是 then 被调用的时候,Promise对象状态是 pending,大部分都是这种情况。这个时候就需要把then的回调推入Promise实例对象的执行队列中,等待状态转变后执行。
  4. then方法返回的是一个新的 Promise 实例。当then的回调有return的时候,如果return的是Promise实例对象的话,那么then后跟的then(实际是then里的回调)就需要等待这个Promise实例对象状态转变后执行,如果return的是其他的话,其他会被Promise.resolve方法包裹,然后立即执行后面的then。
  5. 如果构造函数实例化的时候接受的回调函数里面没有异步操作的话,直接调用了 resolve/reject,在Promise实例化的时候状态就已经转变了成 fulfilled/rejected 了,也就是说then 被调用的时候,状态已经不是 pending 了。
1
PromiesX.prototype.then = function (onResolve,onReject) {
2
    const self = this;
3
    onResolve = typeof onResolve === 'function' ? onResolve : function () {};
4
    onReject = typeof onReject === 'function' ? onReject : function () {};
5
6
    if(self.status === 'pending'){
7
        return new PromiseX ((resolve,reject) => {
8
            // 因为状态未转变,所以成功与失败的回调都需要推进队列
9
            self.onResolvedQueue.push(function(){
10
                try{
11
                    const x = onResolve(self.data);
12
                    if (x instanceof PromiseX) {
13
                        x.then(resovle, reject)
14
                    } else {
15
                        resolve(x)
16
                    }
17
                } catch (e) {
18
                    reject(e)
19
                }
20
            })
21
                
22
            self.onRejectedQueue.push(function(){
23
                try{
24
                    const x = reject(self.data);
25
                    if (x instanceof PromiseX) {
26
                        x.then(resovle, reject)
27
                    } else {
28
                        resolve(x)
29
                    }
30
                } catch (e) {
31
                    reject(e)
32
                }
33
            })
34
                
35
        })
36
    }
37
38
    if (self.status === 'fulfilled') {
39
        return new Promise((resolve,reject) => {
40
            try{
41
                const x = onResolve(self.data);
42
                if (x instanceof PromiseX) {
43
                    x.then(resovle, reject)
44
                } else {
45
                    resolve(x)
46
                }
47
            } catch (e) {
48
                reject(e)
49
            }
50
        })
51
    }
52
53
    if (self.status === 'fulfilled') {
54
        return new Promise((resolve,reject) => {
55
            try{
56
                const x = reject(self.data);
57
                if (x instanceof PromiseX) {
58
                    x.then(resovle, reject)
59
                } else {
60
                    resolve(x)
61
                }
62
            } catch (e) {
63
                reject(e)
64
            }
65
        })
66
    }
67
}
68
69
// catch实际上就是then方法第一个参数传null
70
Promise.prototype.catch = function (onRejected) {
71
    return this.then(null, onRejected)
72
}

穿透

上面基本上就是一个基本能用的Promise实现了,看一下下面的代码,运行完成是可以alert 8的,也就是说8 穿透了多个 then 方法。但实际上,上面的代码是 undefined

1
new Promise(resolve=>resolve(8))
2
  .then()
3
  .then()
4
  .then(function foo(value) {
5
    alert(value)
6
  })

那么问题出在了哪里呢,实际上是中间的then方法没有接到参数,根据下面的代码,onResolve
和onReject没有传,所以赋值为空function啦,只要把参数向下传递就好了
1
onResolve = typeof onResolve === 'function' ? onResolve : function () {};
2
onReject = typeof onReject === 'function' ? onReject : function () {};

改成
1
onResolve = typeof onResolve === 'function' ? onResolve : value => value;
2
onReject = typeof onReject === 'function' ? onReject : reason => {throw reason};

其他Promise方法

Promise.resolve

返回一个fulfilled状态的promise对象

1
Promise.resolve = function (reason) {
2
    return new Promise((resolve,reject) => {
3
        resolve(reason)
4
    })
5
}

Promise.rejected

返回一个rejected状态的promise对象

1
Promise.reject = function (reason) {
2
    return new Promise((resolve,reject) => {
3
        reject(reason)
4
    })
5
}

Promise.all

接收一个promise对象数组为参数 只有全部 promise 进入 fulfilled 状态才会继续后面的处理 通常会用来处理 多个并行异步操作

1
Promise.all = function ([p1,p2,p3]) {
2
  return new Promise((resolve, reject) => {
3
    let values = []
4
    let count = 0
5
    [p1,p2,p3].forEach((promise, index) => {
6
      promise.then(value => {
7
        console.log('value:', value, 'index:', index)
8
        values[index] = value
9
        count++
10
        if (count === [p1,p2,p3].length) {
11
          resolve(values)
12
        }
13
      }, reject)
14
    })
15
  })
16
}

Promise.race

接收一个promise对象数组为参数 Promise.race 只要有一个promise对象进入 fulfilled 或者 rejected 状态的话,就会继续进行后面的处理

1
Promise.race = function ([p1,p2,p3]) {
2
  return new Promise((resolve, reject) => {
3
      [p1,p2,p3].forEach((promise) => {
4
         promise.then(resolve, reject);
5
      });
6
  });
7
}

最后完整的代码

原则上,promise.then(onResolved, onRejected)里的这两个函数需要异步调用,关于这一点,标准里也有说明:

In practice, this requirement ensures that onFulfilled and onRejected execute asynchronously, after the event loop turn in which then is called, and with a fresh stack.

所以我们需要对我们的代码做一点变动,即在四个地方加上setTimeout(fn, 0)

Tip: 我们这里增加setTimeout,涉及到js执行栈以及js单线程和eventLoop相关的知识,各位对js的执行栈、js单线程、eventLoop不太了解的,可以谷歌查阅下相关资料,后边我有空也会写一篇js执行栈、js的单线程、eventloop的讲解文章。下面的代码中,我也会简单写一些加入setTimeout的原因分析。

1
function PromiseX (fn) {
2
    const self = this;
3
    self.status = 'pending';
4
    self.data = '';
5
    self.onResolvedQueue = [];
6
    self.onRejectedQueue = [];
7
8
    try{
9
        fn(resolve,reject)
10
    } catch (e) {
11
        reject(e)
12
    }
13
14
    function resolve (value) { //value成功态时接收的终值
15
        if (value instanceof PromiseX) {
16
            value.then(resolve, reject);
17
            return;
18
        }
19
20
        // 为什么resolve 加setTimeout? 见下文
21
        setTimeout(()=>{
22
            if (selft.status === 'pending') {
23
                self.status = 'fulfilled';
24
                self.data = value;
25
                self.onResolvedQueue.forEach(resolved => resolved(value));
26
            }
27
        }, 0)
28
29
    }
30
    function reject (reason) {
31
        setTimeout(() => {
32
            if (selft.status === 'pending') {
33
                self.status = 'rejected';
34
                self.data = reason;
35
                self.onRejectedQueue.forEach(rejected => rejected(reason));
36
            }
37
        }, 0);
38
    }
39
}
40
41
PromiesX.prototype.then = function (onResolve,onReject) {
42
    const self = this;
43
    onResolve = typeof onResolve === 'function' ? onResolve : value => value;
44
    onReject = typeof onReject === 'function' ? onReject : reason => {throw reason};
45
46
    if(self.status === 'pending'){
47
        return new PromiseX ((resolve,reject) => {
48
            // 因为状态未转变,所以成功与失败的回调都需要推进队列
49
            self.onResolvedQueue.push(function(){
50
                try{
51
                    const x = onResolve(self.data);
52
                    if (x instanceof PromiseX) {
53
                        x.then(resovle, reject)
54
                    } else {
55
                        resolve(x)
56
                    }
57
                } catch (e) {
58
                    reject(e)
59
                }
60
            })
61
                
62
            self.onRejectedQueue.push(function(){
63
                try{
64
                    const x = reject(self.data);
65
                    if (x instanceof PromiseX) {
66
                        x.then(resovle, reject)
67
                    } else {
68
                        resolve(x)
69
                    }
70
                } catch (e) {
71
                    reject(e)
72
                }
73
            })
74
                
75
        })
76
    }
77
    // then里面的fulfilled/rejected状态时 为什么要加setTimeout ?见下文
78
    if (self.status === 'fulfilled') {
79
        return new Promise((resolve,reject) => {
80
            setTimeout(() => {
81
                try{
82
                    const x = onResolve(self.data);
83
                    if (x instanceof PromiseX) {
84
                        x.then(resovle, reject)
85
                    } else {
86
                        resolve(x)
87
                    }
88
                } catch (e) {
89
                    reject(e)
90
                }
91
            }, 0);
92
        })
93
    }
94
95
    if (self.status === 'fulfilled') {
96
        return new Promise((resolve,reject) => {
97
            setTimeout(() => {
98
                try{
99
                    const x = reject(self.data);
100
                    if (x instanceof PromiseX) {
101
                        x.then(resovle, reject)
102
                    } else {
103
                        resolve(x)
104
                    }
105
                } catch (e) {
106
                    reject(e)
107
                }
108
            }, 0);
109
110
        })
111
    }
112
}
113
114
// catch实际上就是then方法第一个参数传null
115
Promise.prototype.catch = function (onRejected) {
116
    return this.then(null, onRejected)
117
}
118
119
Promise.resolve = function (reason) {
120
    return new Promise((resolve,reject) => {
121
        resolve(reason)
122
    })
123
}
124
Promise.reject = function (reason) {
125
    return new Promise((resolve,reject) => {
126
        reject(reason)
127
    })
128
}
129
Promise.all = function ([p1,p2,p3]) {
130
  return new Promise((resolve, reject) => {
131
    let values = []
132
    let count = 0
133
    [p1,p2,p3].forEach((promise, index) => {
134
      promise.then(value => {
135
        console.log('value:', value, 'index:', index)
136
        values[index] = value
137
        count++
138
        if (count === [p1,p2,p3].length) {
139
          resolve(values)
140
        }
141
      }, reject)
142
    })
143
  })
144
}
145
Promise.race = function ([p1,p2,p3]) {
146
  return new Promise((resolve, reject) => {
147
      [p1,p2,p3].forEach((promise) => {
148
         promise.then(resolve, reject);
149
      });
150
  });
151
}

问题

  • 为什么构造函数中 self.onResolvedQueue 和 self.onRejectedQueue 要用数组 ?

当下面这样非链式调用的时候会出现多个回调的情况

1
const promise1 = new Promise((resolve,reject) => {
2
    ...
3
});
4
5
promise1.then((value) => {
6
    console.log(value)
7
})
8
9
promise1.then((value) => {
10
    console.log(value)
11
})

  • 为什么resolve 加setTimeout?

原因:

  1. 2.2.4规范 onFulfilled 和 onRejected 只允许在 execution context 栈仅包含平台代码时运行.

  2. 这里的平台代码指的是引擎、环境以及 promise 的实施代码。实践中要确保 onFulfilled 和 onRejected 方法异步执行,且应该在 then 方法被调用的那一轮事件循环之后的新执行栈中执行。

  • then里面的FULFILLED/REJECTED状态时 为什么要加setTimeout ?

原因:

  1. 其一 2.2.4规范 要确保 onFulfilled 和 onRejected 方法异步执行(且应该在 then 方法被调用的那一轮事件循环之后的新执行栈中执行) 所以要在resolve里加上setTimeout

  2. 其二 2.2.6规范 对于一个promise,它的then方法可以调用多次.(当在其他程序中多次调用同一个promise的then时 由于之前状态已经为fulfilled/rejected状态,则会走的下面逻辑),所以要确保为fulfilled/rejected状态后 也要异步执行onFulfilled/onRejected

  3. 其三 2.2.6规范 也是resolve函数里加setTimeout的原因总之都是 让then方法异步执行 也就是确保onFulfilled/onRejected异步执行

参考


notice

欢迎访问 xiaopf 的博客, 若有问题或者有好的建议欢迎留言,笔者看到之后会及时回复。 评论点赞需要github账号登录,如果没有账号的话请点击 github 注册, 谢谢 !

If you like this blog or find it useful for you, you are welcome to comment on it. You are also welcome to share this blog, so that more people can participate in it. If the images used in the blog infringe your copyright, please contact the author to delete them. Thank you !