我正在参加「掘金·启航计划」
前言
在前两篇文章:开篇 - 你对 Promise 的基本特性了解多少、基础篇 - 3 个对象方法和 6 个类方法 里我们已经清楚了 Promise 的基本特性和其方法的使用 。
这节我们就来实现一个简单的 Promise,这不仅能帮助我们巩固前面掌握的知识,也可以更好地理解 Promise 各个方法的使用原理,以便在实践中更好地运用 Promise。
功能分析
在上一篇中学习了 Promise 的 3 个对象方法和 6 个类方法,我们要实现一个自己的 Promise (下面我暂时帮它命名为 doPromise
),也需要实现这些方法。
通常给一个类命名,单词首字母需要大写;我是觉得这样看起来更顺眼,大家不要学我
核心功能手写实现
constructor 构造器
Promise 处理回调的参数是必不可少的,这里先定义两个用于处理成功和失败的回调函数
class doPromise {
constructor(executor) {
const resolve = (value) => {}
const reject = (reason) => {}
}
}
特性一:立即执行
代码实现非常简单,我们知道 constructor
是 class 中的一个特殊方法,在使用 new
关键字创建或初始化某个对象时,JavaScript 会立即调用对象的 constructor
方法
所以我们只需要在 constructor 配置回调函数,它会在我们创建 Promise 后立即执行
class doPromise {
constructor(executor) {
console.log('我现在就要执行!!'); // 给大家解释一下可以执行,后续就删除掉了
const resolve = (value) => {}
const reject = (reason) => {}
executor(resolve, reject)
}
}
new doPromise(() => {}) // 我现在就要执行!!
特性二:三种状态的变更
我们知道在 Promise 中有三种状态:pending
、fulfilled
、rejected
,分别表示 Promise 的初始状态,没有被敲定也没有被拒绝;已敲定状态,表示 Promise 执行成功;已拒绝状态,表示 Promise 执行失败。
为实现三种状态的变更,我们可以定义 三个变量用来控制这些状态
const PROMISE_STATUS_PENDING = 'pending'
const PROMISE_STATUS_FULFILLED = 'fulfilled'
const PROMISE_STATUS_REJECTED = 'rejected'
class doPromise {
constructor(executor) {
this.status = PROMISE_STATUS_PENDING // 初始状态为 pending
const resolve = (value) => {
this.status = PROMISE_STATUS_FULFILLED // 执行成功后,状态变更为 fulfilled
}
const reject = (reason) => {
this.status = PROMISE_STATUS_REJECTED // 执行失败后,状态变更为 rejected
}
executor(resolve, reject)
}
}
特性三:状态不可逆
Promise 的状态一旦由 pending
转变为 fulfilled
或者 rejected
,则状态锁定,后续任何调用都不会变更此状态。
所以,我们只需要在调用 resolve() 或 reject() 变更状态前,判断此时是否为 pending
状态即可:
const resolve = (value) => {
if (this.status === PROMISE_STATUS_PENDING) {
this.status = PROMISE_STATUS_FULFILLED
}
}
const reject = (reason) => {
if (this.status === PROMISE_STATUS_PENDING) {
this.status = PROMISE_STATUS_REJECTED
}
}
3 个对象方法的实现
1. then() 方法
then()
方法是 Promise 中最复杂的一个方法,需要考虑到的因素是最多的
我们先来回顾一下 Promise 中的 then() 中通常是怎样的代码结构 👇
then()
中接收两个可选参数 onFulfilled 和 onRejected 回调函数。
还需要定义两个值,value
和 reason
分别用于接收 执行成功的结果 和 执行失败的原因
在执行成功时,调用 resolve ,并把接收的值赋给 value;执行被拒绝时,调用 reject,拒绝原因赋值给 reason
其实在 resolve 和 reject 中赋值的代码不应该在这一部分写,而是在构造器中就已经应该存在的逻辑。但是,如果一个 promise 没有调用 then 方法,只是简单的定义,那么给 value 赋值也没有实际意义
总之,下面我们需要实现两部分功能:
-
在 resolve 和 reject 中 获取到 Promise 的结果并保存
-
在
then()
的 第一个回调函数 (onFulfilled) 中返回 value 的值(仅当状态为 fulfilled),第二个回调函数(onRejected)中返回 reason 原因(仅当状态为 rejected)
class doPromise {
constructor(executor) {
this.status = PROMISE_STATUS_PENDING
this.value = undefined
this.reason = undefined
const resolve = (value) => {
this.status = PROMISE_STATUS_FULFILLED
this.value = value
this.onFulfilled(this.value)
}
const reject = (reason) => {
this.status = PROMISE_STATUS_REJECTED
this.reason = reason
this.onRejected(this.reason)
}
executor(resolve, reject)
}
then(onFulfilled, onRejected) {
this.onFulfilled = onFulfilled
this.onRejected = onRejected
}
}
TypeError: this.onFulfilled is not a function
是的,当你这个时候使用 promise.then(...)
方法去尝试执行的时候会报上面错误
原因:代码会首先执行 constructor 中的内容,此时 then() 方法还未执行,所以 this.onFulfilled 是 undefined
,并不是一个函数
解决方法:
我们知道,setTimeout 可以把自身的内容作为异步执行,先放入 JavaScript 的任务队列里,在同步任务执行完成后,才会继续从任务队列中取出继续执行。
如果我们用 setTimeout 把 onFullfilled() 调用包裹起来,那么 onFullfilled() 就可以在同步任务即 then() 之后再进行调用,也就能获取到 then() 方法传递的两个回调函数赋值给 onFullfilled() 和 onRejected()
微任务 queueMicrotask
🧊 值得注意的是,根据我们的使用习惯也是为了方便理解,我刚刚使用了 setTimeout 作为异步说明的论证,但 Promise 调用后本身是一个微任务,setTimeout 属于宏任务,所以我们后续使用 queueMicrotask()
替换 setTimeout()
。queueMicrotask 在此处不作讲解。
多次调用
链式调用 我们知道,那什么是 多次调用 呢 ?
就比如下面这样:
const p1 = new doPromise((resolve, reject) => {
resolve(200)
})
p1.then(res => {
console.log(res);
}, err => {...})
p1.then(res => {
console.log(res);
}, err => {...})
// 1
理论上来说,或者你尝试一下 Promise 就知道,正常情况下应该会打印 两次 resolve 结果 200
,然而我们这里只会返回一次,思考下为什么 ?
还记得我们刚刚用了异步代码 queueMicrotask
吗,我们来看一下代码的具体执行情况:
- 创建 doPromise 实例并回调
resolve(200)
,此时状态由 pending 转变为 fulfilled,value 被赋值 200,queueMicrotask 添加到微任务队列,向后执行。 - p1 第一次调用 then() 方法,打印 console.log 内容,并且 this.onFulfilled 被赋值,由于后续仍有同步任务,继续向后执行。
- p1 第二次调用 then() 方法,打印 console.log 内容,但上一次 this.onFulfilled 的值被覆盖。
- 执行异步代码,调用 this.onFulfilled
这就是我们的 Promise 为什么只会打印一次结果的原因,要实现多次调用应该如何调整代码呢?
这还不简单,既然是因为覆盖造成的,那我们不让它覆盖不就完了,有几次调用就创建几个异步任务。用一个 数组
分别接收执行成功和失败的回调,并在 回调中遍历执行 每个异步任务。
class doPromise {
constructor(executor) {
// ... 之前的省略,仅更新本次添加的内容
this.onFulfilledFns = []
this.onRejectedFns = []
const resolve = (value) => {
if (this.status === PROMISE_STATUS_PENDING) {
this.status = PROMISE_STATUS_FULFILLED
this.value = value
queueMicrotask(() => {
this.onFulfilledFns.forEach(fn => {
fn(this.value) // 遍历每个回调
})
});
}
}
// ... reject 同理,此处省略
executor(resolve, reject)
}
then(onFulfilled, onRejected) {
this.onFulfilledFns.push(onFulfilled) // 将每个方法保存到数组中
this.onRejectedFns.push(onRejected)
}
}
这次看起来逻辑就通顺了,它不是同步任务吗,管他几次调用,我们就在 then() 方法中把调用 push 到提前准备好的数组,等同步任务完成后,一个个遍历出来回调的内容再传值就 ok 了。
🔨 特殊情况:多次且异步调用
在调用成功后,再异步调用,是否还能输出正确的结果 ?
const p1 = new Promise((resolve, reject) => {
resolve(200)
})
p1.then(res => {
console.log(res);
}, err => {...})
setTimeout(() => {
p1.then(res => {
console.log(res);
}, err => {...})
}, 1000) // 延迟 1s 后再次调用 p1
// 200
我们发现本应该输出两次的结果,此时又只出现了一次。这是因为,在 setTimeout() 中的 p1 执行 then() 方法时,p1 的状态已经变成 fulfilled,即便把回调添加到了数组中,也没办法再次执行了。
实际上在第一次调用 then() 之后,我们已经得到了对应的状态以及 value/reason 的值。该有的都有了,为什么不直接执行回调呢。所以我们可以这样解决:
then(onFulfilled, onRejected) {
// 如果此时状态已经变成 fulfilled/rejected,说明已经有 value/reason,请大胆地执行该回调
if (this.status === PROMISE_STATUS_FULFILLED && onFulfilled) {
onFulfilled(this.value)
}
if (this.status === PROMISE_STATUS_REJECTED && onRejected) {
onRejected(this.reason)
}
// 仅当状态是 pending 时,向数组中 push 回调
if (this.status === PROMISE_STATUS_PENDING) {
this.onFulfilledFns.push(onFulfilled)
this.onRejectedFns.push(onRejected)
}
}
🔨🔨 你以为这就完事啦?NO!
刚刚新增的两条代码:在 then() 中进行状态的判断,当状态不是 pending 时,马上执行回调
是不是觉得有点不对劲,但又说不上来的感觉。明明已经做好了功能,逻辑说得通,测试没问题呀。执行回调的结果都可以正确拿到,调用次数也对得上。
我们再来看一下刚刚加完代码的逻辑:
- 我们在调用 resolve 之后,状态立即变更为 fulfilled,value 也被同步赋值;
- 此时调用 then() 对象方法,根据状态判断进入第一个作用域,直接执行传入的回调函数
得到的结果可能是正确的,但是我们会发现在 then()
方法中永远无法进入到 pending 状态的作用域中,那之前用来异步执行的 queueMicrotask
函数岂不是失去了意义。这样实质上会出现的问题是:
const p1 = new Promise((resolve, reject) => {
resolve(200)
})
p1.then(res => {
console.log(res)
}, err => {...})
console.log(10086)
// 200
// 10086
现在这个 Promise 不是异步的了! 那还是正经 Promise 吗?!
我们需要把状态判断和赋值的操作也转移到异步任务中,这样才不会在 then() 中被提前处理,最终处理 then() 方法的多次调用逻辑需要这样调整:
class doPromise {
constructor(executor) {
// ... 之前的省略,仅更新本次添加的内容
const resolve = (value) => {
if (this.status === PROMISE_STATUS_PENDING) {
// 状态判断、赋值全部移到异步任务中
queueMicrotask(() => {
if (this.status !== PROMISE_STATUS_PENDING) return
this.status = PROMISE_STATUS_FULFILLED
this.value = value
this.onFulfilledFns.forEach(fn => {
fn(this.value) // 遍历每个回调
})
});
}
}
// ... rejected 同理,此处省略
}
}
链式调用
解决完多次调用的问题,就要来思考链式调用的实现方法了。毕竟 Promise 的出现有一大部分原因是为了 处理回调地狱,而 链式调用 就是解决这个问题的一大方法。
那么我们首先思考通常怎样使用链式调用,它的原理又是什么?
使用链式调用,通常是由于获取最终数据的参数依赖于另一个请求,总之是一步无法完成完整的业务内容。
所以,Promise 链式调用的本质: then()
方法的返回值,仍是一个 Promise 实例。只有这样,后续才能接着调用 then() 方法和 catch() 方法
把 then() 方法中的内容用我们自己的 Pormise 包裹起来,再把 return 出的返回值 resolve 出去。
then(onFulfilled, onRejected) {
return new doPromise((resolve, reject) => {
if (this.status === PROMISE_STATUS_FULFILLED && onFulfilled) {
const value = onFulfilled(this.value)
resolve(value)
}
if (this.status === PROMISE_STATUS_REJECTED && onRejected) {
const reason = onRejected(this.reason)
resolve(reason)
}
if (this.status === PROMISE_STATUS_PENDING) {
this.onFulfilledFns.push(() => {
const value = onFulfilled(this.value)
resolve(value)
})
this.onRejectedFns.push(() => {
const reason = onRejected(this.reason)
resolve(reason)
})
}
})
}
错误处理
错误处理分很多种,包括自身抛出的异常和代码运行的异常
当在 Promise 中发生异常错误时,我们可以使用 try/catch
捕获并在 constructor 中执行 reject()
class doPromise {
constructor(executor) {
// ... 省略 resolve、rejected 等代码
try {
executor(resolve, reject)
} catch (err) {
reject(err)
}
}
}
由于刚刚在实现链式调用时,我们把 then() 的返回值也包装成了一个 Promise,所以在 then() 中也应该做对应的错误处理
then(onFulfilled, onRejected) {
return new doPromise((resolve, reject) => {
if (this.status === PROMISE_STATUS_FULFILLED && onFulfilled) {
try {
const value = onFulfilled(this.value)
resolve(value)
} catch (err) {
reject(err)
}
}
// ... 后面也是一样的处理方式
})
}
后面的代码也是一样的处理方式,我们可以定义一个错误处理方法来优化代码:
function execFunctionWithCatchError(execFn, value, resolve, reject) {
try {
const result = execFn(value)
resolve(result)
} catch(err) {
reject(err)
}
}
将定义好的方法使用在 then() 中就可以这么写
then(onFulfilled, onRejected) {
return new doPromise((resolve, reject) => {
if (this.status === PROMISE_STATUS_FULFILLED && onFulfilled) {
execFunctionWithCatchError(onFulfilled, this.value, resolve, reject)
}
if (this.status === PROMISE_STATUS_REJECTED && onRejected) {
execFunctionWithCatchError(onRejected, this.reason, resolve, reject)
}
if (this.status === PROMISE_STATUS_PENDING) {
this.onFulfilledFns.push(() => {
execFunctionWithCatchError(onFulfilled, this.value, resolve, reject)
})
this.onRejectedFns.push(() => {
execFunctionWithCatchError(onRejected, this.reason, resolve, reject)
})
}
})
}
2. catch() 方法
通过上一节的内容其实我们知道,catch() 方法的根本就是 then() 方法第二个回调函数的语法糖
它可以让我们在 then() 方法中专心关注 fulfilled(敲定状态)返回的值,在 catch() 方法中处理 Promise 执行过程中出现的异步错误。
catch(onRejected) {
return this.then(undefined, onRejected)
}
不过我们还需要在 then() 方法中处理回调函数为 undefined 的情况
then(onFulfilled, onRejected) {
// 定义两个默认的回调函数,用于处理在未传回调时的默认处理方式
// 1. 如果 onRejected 未传值,则抛出异常给下一个 Promise 处理
const defaultOnRejected = err => { throw err }
onRejected = onRejected || defaultOnRejected
// 2. 如果 onFulfilled 未传值,则返回结果给下一个 Promise 处理
const defaultOnFulfilled = value => { return value }
onFulfilled = onFulfilled || defaultOnFulfilled
return new doPromise((resolve, reject) => {
// ...
if (this.status === PROMISE_STATUS_PENDING) {
// 添加可选参数判断,防止在没有回调时报错
if (onFulfilled) this.onFulfilledFns.push(() => {
execFunctionWithCatchError(onFulfilled, this.value, resolve, reject)
})
if (onRejected) this.onRejectedFns.push(() => {
execFunctionWithCatchError(onRejected, this.reason, resolve, reject)
})
}
})
}
3. finally()
通过上面 catch() 方法的实现,再来思考 finally() 的话就会清晰很多
finally()
主要的作用就是在 Pormise 执行完成后调用的函数,它无须接收任何参数
finally(onFinally) {
// 无论状态是敲定还是拒绝,都执行传入的 onFinally 方法
this.then(() => {
onFinally()
}, () => {
onFinally()
})
}
6 个类方法的实现
类方法需要加上 static 前缀
1. Promise.resolve()
static resolve(value) {
return new doPromise(resolve => resolve(value))
}
2. Promise.reject()
static reject(reason) {
return new doPromise((resolve, reject) => reject(reason))
}
3. Promise.all()
all()
方法的使用是在 Promise 中出现频率较高的一个,也是面试中常考的问题
而且在网站的性能优化中,也常用来并行加载多个资源,优化页面加载速度和用户体验
all()
方法使用原理分析:
- all() 接收一组 Promise
- Promises 全部执行成功,在最后一个 Promise 状态敲定时,返回这组 Promises 的所有执行结果(返回的执行结果顺序由数组内 Promise 顺序决定,不受 resolve 的先后影响)
- Promises 中任意一个执行失败,立即返回拒绝原因
因为 Promise.all()
的结果也要在 then() 方法中接收和处理结果,所以 all() 方法返回一个 Promise
定义一个数组 results 用于存储 Promises 成功的结果,遍历数组中每个 Promise 的执行结果
全部执行完成且成功时,按照数组的传入顺序返回对应结果,如果发生错误立即返回 reject 错误原因
将数组遍历执行其实很好理解,这里需要注意的是两点:
- 执行成功时,按照数组的传入顺序返回对应结果
- 如果传入的不是 promise,由于没有 then 方法调用时会报错,所以直接返回原值
static all(promises) {
return new doPromise((resolve, reject) => {
const results = [] // 接收 Promises 成功的结果
let fulfilledTimes = 0 // 执行成功的次数
const pushResult = (_index, _value) => {
results[_index] = _value
fulfilledTimes++
// 若执行成功的次数和Promises数组长度相等时,返回结果
if (fulfilledTimes === promises.length) {
resolve(results)
}
}
promises.forEach((promise, index) => {
if (promise instanceof doPromise) {
promise.then(res => {
pushResult(index, res)
}, err => {
reject(err)
})
} else {
pushResult(index, promise)
}
})
})
}
4. Promise.allSettled()
all()
和 allSettled()
的区别在于,allSettled()
不会将错误暴露出来,而是和所有结果一起 resolve 出来
有了上面的经验,allSettled()
就容易理解了
在 Promise 被拒绝时,也将结果保存在数组中,等待全部 Promises 执行完成,一起返回其结果
static allSettled(promises) {
return new doPromise(resolve => {
const results = []
let fulfilledNum = 0
promises.forEach((promise, index) => {
if (promise instanceof doPromise) {
promise.then(res => {
// 只是把 results 数组中的值换成了对象
results[index] = { status: PROMISE_STATUS_FULFILLED, value: res }
fulfilledNum++
if (fulfilledNum === promises.length) {
resolve(results)
}
}, err => {
// promise 被拒绝时不影响后续执行,只将状态和原因进行保存
results[index] = { status: PROMISE_STATUS_REJECTED, value: err }
fulfilledNum++
if (fulfilledNum === promises.length) {
resolve(results)
}
})
} else {
results[index] = { status: PROMISE_STATUS_FULFILLED, value: promise }
fulfilledNum++
}
})
})
}
5. Promise.race()
无论结果如何,只要第一个出来的结果
这里就不再需要定义数组接收了,因为 race()
只返回一个结果
这样就更容易了,我们只需要拿到最先出结果的 promise ,根据其状态调用不同的回调函数就可以
static race(promises) {
return new doPromise((resolve, reject) => {
promises.forEach(promise => {
if (promise instanceof doPromise) {
// 这些 promise 都是异步的,最快调用 then 方法的 promise 结果作为返回值
promise.then(res => {
resolve(res)
}, err => {
reject(err)
})
} else {
// 普通值直接返回原值
resolve(promise)
}
})
})
}
6. Promise.any()
any()
和 race()
的区别在于两点
- 只返回最先 成功 的那一条结果
- 如果这一组的所有 promise 都被拒绝,返回错误提示
当所有 promise 都被拒绝时,返回错误,这就说明了需要有一个数组去接收错误
第一个回调 resolve 和 race()
一样,只需返回最快成功的 promise
static any(promises) {
const reasons = [] // 接收拒绝原因,在所有 promise 都被拒绝时返回
return new doPromise((resolve, reject) => {
promises.forEach(promise => {
if (promise instanceof doPromise) {
promise.then(res => {
resolve(res)
}, err => {
reasons.push(err)
if (reasons.length === promises.length) {
reject(new AggregateError([reasons], 'All Promises rejected'))
}
})
} else {
resolve(promise)
}
})
})
}
结语
到这里,实现一个简单的 Promise 已经结束了;这虽然不是一个完整的 Promise,还有很多情况在上面没有考虑到(大家可以自己补充也可以发表在评论里帮助其他同学),不过也是对 Promise 系列前两篇文章知识点的一个巩固,希望大家能够更好的理解 Promise 对象。
也十分感谢小伙伴们能看到这里,如果文中有我理解错误或者不对的地方也希望大家能帮我指出错误,不要让我误人子弟 😳
后续在准备面试的时候,应该会再开一个面试题系列专栏,到时候会收集一些 Promise 相关的面试题,汇总一下。