前言
最近学习Promise
的实现原理,看了
异步编程二三事 | Promise/async/Generator实现原理解析 | 9k字
这两篇文章,秉承着实践是最好的学习方式这一真理,决定自己也来动手实现一遍。 这篇文章更多是记录下自己的思考历程,很多东西在上面两篇文章中也解释的很详细了,我这里只是加了点自己学习中的思考~
Promise 和 回调 的不同
异步中最常见的场景就是 ajax
,我们需要拿到请求回来的数据后再对这个数据进行相应的处理。
在传统的使用回调函数处理异步得到的数据方案中,有时会因需要依赖异步得到的数据再进行异步调用去获取数据(如,使用get
请求得到的数据发送post
请求等)而出现回调地狱的情况,使代码惨不忍睹,为了解决回调地狱的问题,Promise应运而生。
下面来看看对于一个异步调用,使用回调函数和使用Promise的区别
// 回调函数
const callbackFn = (callback) => {
setTimeout(() => {
// 在这里异步获取到 data
callback('data')
}, 1000)
}
callbackFn((val) => console.log(val)) // 1s 后输出 2
// 使用 Promise
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('data')
}, 1000)
})
p1.then(res => console.log(res))
现在来一步步分析其中的区别
- 回调函数方案的调用过程
- 在
callbackFn
中,1s 后调用参数中的callback
并传入参数data
- 在调用
callbackFn
时,把相应的处理函数作为参数传入 - 调用
callbackFn
,异步任务被执行,触发回调函数
- 在
- Promise 方案的调用过程
Promise
的构造方案接收一个executor()
,在new Promise()
时就立刻执行这个executor回调executor()
内部的异步任务被放入宏/微任务队列,等待执行then()
被执行,收集成功/失败回调,放入成功/失败队列executor()
的异步任务被执行,触发resolve/reject
,从成功/失败队列中取出回调依次执行
从上面可以得出第一个大不同点就是——
- 回调函数方案中,调用
callbackFn
时,setTimeout
才开始,同时在这时传入回调函数 Promise
方案中,new Promise
时,setTimeout
就开始了,then时才传入回调函数
实现 Promise
Promise 的一个大致框架是,new Promise -> then() 收集回调 -> resolve/reject 执行回调
根据以上思路,先来实现一个破产版的 Promise
class MyPromise {
constructor (executor) {
this._resolveQueue = [] // then 收集的执行成功的回调队列
this._rejectQueue = [] // then 收集的执行失败的回调队列
let _resolve = (val) => {
while (this._resolveQueue.length) {
// 从成功队列中取出回调执行
const callback = this._resolveQueue.shift()
callback(val)
}
}
// 实现同resolve
let _reject = (val) => {
while (this._rejectQueue.length) {
const callback = this._rejectQueue.shift()
callback(val)
}
}
// new Promise() 时立即执行 executor,并传入 resolve 和 reject
executor(_resolve, _reject)
}
// 收集回调函数,push 进队列
then (resolveFn, rejectFn) {
this._resolveQueue.push(resolveFn)
this._rejectQueue.push(rejectFn)
}
}
执行一下
const p1 = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve('data')
}, 1000)
})
p1.then(res => console.log(res)) // 1s 后输出 'data'
但是这个破产版实在是太破产了,如果then
的时候异步任务已经执行完了,那这时这个回调函数就永远都不会执行
例如这样:
const p1 = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve('data')
}, 1000)
})
setTimeout(() => {
p1.then(res => console.log(res)) // 没有输出
}, 2000)
Promise 的三种状态
Promise
本质是一个状态机,且状态只能为以下三种:Pending
(等待态)、Fulfilled
(执行态)、Rejected
(拒绝态),状态的变更是单向的,只能从Pending
->Fulfilled
或Pending
->Rejected
,状态变更不可逆
加入这三种状态之后,处理上面的回调函数不会执行的问题就很简单了,问题本质就是——
在Fulfilled
和Rejected
状态下的回调函数应该怎么办
整理下思路,得出以下结论
- 在
Pengding
状态时,把回调函数push
进队列 - 在
Fulfilled
和Rejected
状态时,立即执行回调函数 - 因此需要在
then
中判断状态 - 因此需要把异步任务的结果保存起来
添加代码后:
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
class MyPromise {
constructor (executor) {
this._state = PENDING // promise 状态
this._value = undefined // 保存异步任务的结果
this._resolveQueue = [] // then 收集的执行成功的回调队列
this._rejectQueue = [] // then 收集的执行失败的回调队列
let _resolve = (val) => {
if (this._state !== PENDING) return
this._state = FULFILLED // 变更状态
this._value = val // 保存结果
while (this._resolveQueue.length) {
// 从成功队列中取出回调执行
const callback = this._resolveQueue.shift()
callback(val)
}
}
// 实现同resolve
let _reject = (val) => {
if (this._state !== PENDING) return
this._state = REJECTED // 变更状态
this._value = val // 保存结果
while (this._rejectQueue.length) {
const callback = this._rejectQueue.shift()
callback(val)
}
}
// new Promise() 时立即执行 executor,并传入 resolve 和 reject
executor(_resolve, _reject)
}
// 收集回调函数
then (resolveFn, rejectFn) {
switch (this._state) {
case PENDING:
this._resolveQueue.push(resolveFn)
this._rejectQueue.push(rejectFn)
break
case FULFILLED:
resolveFn(this._value)
break
case REJECTED:
rejectFn(this._value)
break
}
}
}
这样,一个破产升级版的Promise
就这样实现了,下一步来实现一下链式调用
链式调用
先来使用一下链式调用
const p1 = new Promise((resolve, reject) => {
resolve(1)
})
p1.then(res => {
console.log(res)
// return 为 Promise 时,resolve 的值在下一个 then 中作为它的 res
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(2)
}, 1000)
})
})
.then(res => {
console.log(res)
// return 一个值则在下一个 then 中作为它的 res
return 3
})
.then(res => {
console.log(res)
})
/*
输出
1
2
3
*/
整理下思路
- 想要链式调用,在
then
中需要返回一个Promise
return
为Promise
时,resolve
的值在下一个then
中作为它的res
return
一个值则在下一个then
中作为它的res
then
的回调需要顺序执行,在return
一个Promise
时,我们要等待其状态变更才执行下一个then
- 于是我们需要判断传入
then
中的回调的返回值,所以要对传入then
中的回调函数再包装一下
修改一下then()
then (resolveFn, rejectFn) {
// 返回一个新的 Promise
return new MyPromise((resolve, reject) => {
// 对 resolveFn 进行包装
const fulfilledFn = value => {
try {
// 存储传入回调的返回值
let x = resolveFn(value)
/*
如果传入的回调返回值是 Promise,把这个 Promise 称为 x
把当前 then 返回的 Promise 称为 p
x.then 的参数 resolve 是 p 的参数 resolve
x.then 的 resolve 在 x 的异步任务结束后被触发,同时结果作为参数被传入
于是 p 的异步任务也完成
*/
x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
} catch (e) {
reject(e)
}
}
// 对 rejectFn 包装,与 resolveFn 同理
const rejectedFn = value => {
try {
let x = rejectFn(value)
x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
} catch (e) {
reject(e)
}
}
switch (this._state) {
case PENDING:
this._resolveQueue.push(fulfilledFn)
this._rejectQueue.push(rejectedFn)
break
case FULFILLED:
rejectedFn(this._value)
break
case REJECTED:
rejectedFn(this._value)
break
}
})
}
值穿透
使用一下值穿透
const p1 = new Promise((resolve, reject) => {
resolve(2)
})
p1.then().then().then(res => console.log(res)) // 输出 2
思路:
- 思路其实很清晰明了,在上面的代码中,我们直接默认传入
then
的参数是函数并直接调用了 - 如果参数不是函数的话,我们要把当前
Promise
的结果传递给下一个then
,有以下两种方案- 在新创建的
Promise
中,把传入的参数value
直接resolve
- 重新封装
resolveFn
参数,更改为 返回值为传入的参数 的函数
- 在新创建的
- 两种方案大同小异
接下来两种方案都实现一下:
重新封装参数:
then (resolveFn, rejectFn) {
// 重新封装 resolveFn 和 rejectFn 参数
if (typeof resolveFn !== 'function') resolveFn = value => value
if (typeof rejectFn !== 'function') throw new Error (reason instanceof Error ? reason.message : reason)
// ... 以下代码没有改变
}
在Promise
中resolve
:
then (resolveFn, rejectFn) {
return new MyPromise(resolve, reject) {
const fulfilledFn = value => {
try {
if (typeof resolveFn !== 'function') {
resolve(value)
} else {
// 存储传入回调的返回值
let x = resolveFn(value)
x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
}
} catch (e) {
reject(e)
}
}
// rejectedFn 同理
const fulfilledFn = value => {
try {
if (typeof rejectFn !== 'function') {
rejectFn(value)
} else {
// 存储传入回调的返回值
let x = resolveFn(value)
x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
}
} catch (e) {
reject(e)
}
}
// ...以下省略
}
}
兼容同步任务
前面说到过,Promise 的一个大致框架是 new Promise() -> then() 收集回调 -> resolve/reject 执行回调
,这一顺序是建立在executor() 是异步任务的基础上的,如果executor
是同步任务,还是会出现我们破产版Promise
出现的then
收集的回调不会执行的情况,即执行顺序又变成了new Promise() -> resolve/reject 执行 -> then() 收集回调
针对这一问题,我们只需要给resolve/reject
套一个setTimeout
,让他异步执行就可以了,经过这一步,我们的Promise
基本上就完成了,以下贴出完整代码
完整代码
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
class MyPromise {
constructor (executor) {
this._state = PENDING // promise 状态
this._value = undefined // 保存异步任务的结果
this._resolveQueue = [] // then 收集的执行成功的回调队列
this._rejectQueue = [] // then 收集的执行失败的回调队列
let _resolve = (val) => {
// 兼容同步代码,把执行回调的操作放进 setTimeout 里,变成异步
setTimeout(() => {
if (this._state !== PENDING) return
this._state = FULFILLED // 变更状态
this._value = val // 保存结果
while (this._resolveQueue.length) {
// 从成功队列中取出回调执行
const callback = this._resolveQueue.shift()
callback(val)
}
})
}
// 实现同resolve
let _reject = (val) => {
setTimeout(() => {
if (this._state !== PENDING) return
this._state = REJECTED // 变更状态
this._value = val // 保存结果
while (this._rejectQueue.length) {
const callback = this._rejectQueue.shift()
callback(val)
}
})
}
// new Promise() 时立即执行 executor,并传入 resolve 和 reject
try {
executor(_resolve, _reject)
} catch (e) {
_reject(e)
}
}
// 收集回调函数
then (resolveFn, rejectFn) {
// 重新封装 resolveFn 和 rejectFn 参数
if (typeof resolveFn !== 'function') resolveFn = value => value
if (typeof rejectFn !== 'function') throw new Error (reason instanceof Error ? reason.message : reason)
// 返回一个新的 Promise
return new MyPromise((resolve, reject) => {
// 对 resolveFn 进行包装
const fulfilledFn = value => {
try {
// 存储传入回调的返回值
let x = resolveFn(value)
/*
如果传入的回调返回值是 Promise,把这个 Promise 称为 x
把当前 then 返回的 Promise 称为 p
x.then 的参数 resolve 是 p 的参数 resolve
x.then 的 resolve 在 x 的异步任务结束后被触发,同时结果作为参数被传入
于是 p 的异步任务也完成
*/
x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
} catch (e) {
reject(e)
}
}
// 对 rejectFn 包装,与 resolveFn 同理
const rejectedFn = value => {
try {
let x = rejectFn(value)
x instanceof MyPromise ? x.then(resolve, reject) : resolve(x)
} catch (e) {
reject(e)
}
}
switch (this._state) {
case PENDING:
this._resolveQueue.push(fulfilledFn)
this._rejectQueue.push(rejectedFn)
break
case FULFILLED:
rejectedFn(this._value)
break
case REJECTED:
rejectedFn(this._value)
break
}
})
}
}
其他方法
catch
方法
相当于调用then
方法,但只传入rejected
状态的回调函数
catch (onRejected) {
return this.then(undefined, onRejected)
}
静态resolve
方法
static resolve (value) {
// 如果参数是 MyPromise 示例,直接返回这个实例
if (value instanceof MyPromise) return value
return new MyPromise(resolve => resolve(value))
}
静态reject
方法
static reject (value) {
return new MyPromise((resolve, reject) => reject(value))
}
静态all
方法
static all (list) {
return new MyPromise((resolve, reject) => {
let values = [] // 返回值的集合
let count = 0
for (let [i, p] of list.entries()) {
// 数组参数如果不是 MyPromise 实例,先调用 Mypromise.resolve
this.resolve(p).then(res => {
values[i] = res
count++
// 所有状态都变成 fulfilled 时返回的 MyPromise 状态就变成 fulfilled
if (count === list.length) resolve(values)
}, err => {
// 有一个被 rejected 返回的 MyPromise 状态就变成 rejected
reject(err)
})
}
})
}
静态race
方法
static race (list) {
return new MyPromise((resolve, reject) => {
for (let p of list) {
// 只要有一个实例率先改变状态,新的 MyPromise 的状态就跟着改变
this.resolve(p).then(res => {
resolve(res)
}, err => {
reject(err)
})
}
})
}
finally
方法
不管Promise
对象最后状态如何,都会执行的操作
finally (cb) {
return this.then(
value => MyPromise.resolve(cb()).then(() => value)
reason => MyPromise.resolve(cb()).then(() => throw reason)
)
}
总结
加油呀