逐步递进地手写一个Promise
我看网上的代码都是一步到位的,很少有人会解释某一行代码是怎么来的,这会对新手同学造成很大困扰。咱既然要教,那就把逻辑和道理一步步给说明白,让读者看一遍两遍就能完全搞懂。我将用功能依次递进的三个版本来阐述一个比较完整的 Promise 代码是怎么来的。
版本一
编码前想想 Promise 的规则
- Promise 是一个构造函数,其入参是一个函数execute,这个函数的参数也是两个函数(resolve, reject)。
- Promise 返回一个对象,这个对象有一个?then 函数,这个函数的参数也是两个函数(onFulfilled, onRejected)。
- Promise 是一个状态机,初始状态是 pending,只有调用了 resolve / reject 后,才会变成 fulfilled / rejected,且状态不可逆。
根据以上规则,写出如下源代码
function myPromise(execute) {
this.status = "pending"
this.value = null
this.reason = null
const resolve = value => {
if (this.status === "pending") {
this.value = value
this.status = "fulfilled"
}
}
const reject = reason => {
if (this.status === "pending") {
this.reason = reason
this.status = "rejected"
}
}
execute(resolve, reject)
}
myPromise.prototype.then = function (onFulfilled, onRejected) {
onFulfilled = typeof onFulfilled === "function" ? onFulfilled : value => value
onRejected = typeof onRejected === "function" ? onRejected : reason => reason
if (this.status === "fulfilled") {
onFulfilled(this.value)
}
if (this.status === "rejected") {
onRejected(this.reason)
}
}
// 测试代码
new myPromise((resolve, reject) => {
resolve("hello wsx")
})
.then(res => {
console.log(res)
})
// 测试结果
// hello wsx
但它不是完美的,它有什么问题?
至此,我们实现了一个"尝鲜版"的 Promise,它显然是不完美的,它有什么问题?我们尝试执行以下测试代码
new myPromise((resolve, reject) => {
setTimeout(() => {
resolve('hello wsx');
}, 1000)
}).then(res => {
console.log(res)
})
// 测试结果
// 什么也没返回
执行代码完毕后得知,这个版本的 Promise 只能执行同步代码,因为如果换成异步的(如代码中的 setTimeout),那么内部执行顺序就变为是:execute > then > setTimeout。其中在 then 步骤,根据我们上面写的源代码得知,此时的 Promise 状态还没有变成 fulfilled,所以它无法通过判断,也就无法执行 then 的回调函数,也就无法打印 res。
版本二
在步骤一的基础上继续添加规则
- 发布订阅。知道了版本一的缺陷,我们不能马上执行 then 的回调 onFulfilled / onRejected ,需要把它收集起来,等到 execute 调用了?resolve / reject 执行完毕后,才统一执行。
- onFulfilled 和 onRejected 应该是微任务。
开始写代码
更新位置有3处,其中前2处是新增,第3处是改动,读者也应该按照这三个顺序来理解版本二的变动内容,可先看代码再看以下描述。
第1处,添加了两个数组,是为了实现发布订阅用。如何订阅,如何发布,请看下2处描述。
第2处,新增了 pending 判断,版本一的缺点就是如果 excute 的 resolve 是异步回调的,那么执行了 then 时,此时的状态还是 pending,需要在这个阶段把?onFulfilled /?onRejected 收集(订阅)起来。
第3处,改动了 resolve / reject 的函数提,将原来的逻辑套在微任务里(queueMicrotask),并且在最后遍历新增的两个函数,即发布。
function myPromise(execute) {
this.status = "pending"
this.value = null
this.reason = null
/** 1 */
this.onFulfilledArray = []
this.onRejectedArray = []
const resolve = value => {
/** 3 */
queueMicrotask(() => {
if (this.status === "pending") {
this.value = value
this.status = "fulfilled"
this.onFulfilledArray.forEach(func => func(value))
}
})
}
const reject = reason => {
/** 3 */
queueMicrotask(() => {
if (this.status === "pending") {
this.reason = reason
this.status = "rejected"
this.onRejectedArray.forEach(func => func(value))
}
})
}
execute(resolve, reject)
}
myPromise.prototype.then = function (onFulfilled, onRejected) {
onFulfilled = typeof onFulfilled === "function" ? onFulfilled : value => value
onRejected = typeof onRejected === "function" ? onRejected : reason => reason
if (this.status === "fulfilled") {
onFulfilled(this.value)
}
if (this.status === "rejected") {
onRejected(this.reason)
}
/** 2 */
if (this.status === "pending") {
this.onFulfilledArray.push(onFulfilled)
this.onRejectedArray.push(onRejected)
}
}
至此,我们再去执行版本一的测试代码(异步那个),就没有问题了。
但这不是我们的最终版本
它仍然存在问题,我准备了两段测试代码,其中代码1执行正常,代码2执行异常。异常的原因是,不支持链式调用。然而 Promise 的一大爽点就是链式调用,你可以在 new 了一个 Promise 之后,再紧接着使用 then、catch、finally 等一系列操作。
// 测试代码1
// 使用一个变量保存 Promise, 再分别执行 then
const p = new myPromise((resolve, reject) => {
setTimeout(() => {
resolve('hello wsx');
}, 1000)
})
p.then(res => {
console.log(res)
})
p.then(res => {
console.log(res)
})
// 测试结果,输出了两次,正常
// hello wsx
// hello wsx
// 测试代码2
// 链式调用
new myPromise((resolve, reject) => {
setTimeout(() => {
resolve('hello wsx');
}, 1000)
})
.then(res => {
console.log(res)
})
.then(res => {
console.log(res)
})
// 测试结果
// Uncaught TypeError: Cannot read properties of undefined (reading 'then')
// hello wsx
版本三
再增规则
- 链式调用。执行完 then 后,应该返回一个 新的 Promise。这表示你需要再 new 一个 Promise 并返回出去,这是一定要的,因为根据版本一的第3点描述:Promise 是一个状态机,且不可逆。
- 新 Promise 的 resolve / reject,也需要推到微任务中执行。
根据新增的规则,我们写下最终代码
其中涉及到两处改动,已经使用注释标了出来,读者直接看改动部分即可,主要还是针对新增的两条规则做的适配。
function myPromise(execute) {
this.status = "pending"
this.value = null
this.reason = null
this.onFulfilledArray = []
this.onRejectedArray = []
const resolve = value => {
queueMicrotask(() => {
if (this.status === "pending") {
this.value = value
this.status = "fulfilled"
this.onFulfilledArray.forEach(func => func(value))
}
})
}
const reject = reason => {
queueMicrotask(() => {
if (this.status === "pending") {
this.reason = reason
this.status = "rejected"
this.onRejectedArray.forEach(func => func(value))
}
})
}
execute(resolve, reject)
}
myPromise.prototype.then = function (onFulfilled, onRejected) {
onFulfilled = typeof onFulfilled === "function" ? onFulfilled : value => value
onRejected = typeof onRejected === "function" ? onRejected : reason => reason
if (this.status === "fulfilled") {
/** 1 */
return new myPromise((resolve, reject) => {
queueMicrotask(() => {
try {
const result = onFulfilled(this.value)
resolve(result)
}
catch(err) {
reject(err)
}
})
})
}
if (this.status === "rejected") {
/** 1 */
return new myPromise((resolve, reject) => {
queueMicrotask(() => {
try {
const result = onRejected(this.value)
resolve(result)
}
catch(err) {
reject(err)
}
})
})
}
if (this.status === "pending") {
/** 2 */
return new myPromise((resolve, reject) => {
this.onFulfilledArray.push(() => {
queueMicrotask(() => {
try {
const result = onFulfilled(this.value)
resolve(result)
}
catch(err) {
reject(err)
}
})
})
})
this.onRejectedArray.push(() => {
queueMicrotask(() => {
try {
const result = onRejected(this.value)
resolve(result)
}
catch(err) {
reject(err)
}
})
})
}
}
这样,我们再套用版本二最后的测试代码,就能正常执行了,现在的版本3已经支持链式调用。到目前为止,已经实现了功能递进的三个版本,已经对 Promise 有了 80%的理解,其实已经足够了,不管是应对面试,还是在工作中使用,都是搓搓有余的。
剩下的 20% 是什么?
当然肯定还有读者会问,剩下的20%是什么?其实剩下的是一个规范问题,读者可自行上网搜索有关 Promise/A+ 的内容,这就是我所说的20%,简单来说就是一些大家需要约定俗成的东西,或者一些约束。比如版本一中的第3点
Promise 是一个状态机,初始状态是 pending,只有调用了 resolve / reject 后,才会变成 fulfilled / rejected,且状态不可逆。
就是一个规范 ,它就是一个状态机,且状态不可逆,所以你在源码就得这么实现。
还有 then 返回的仍是一个 Promise(resolve, reject),如果调用?resolve 时传入参数也是一个 Promise,那么该如何如何遵循规则。
这些都是属于规范的东西,有兴趣的读者可以读一下相关内容。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!