共计 6717 个字符,预计需要花费 17 分钟才能阅读完成。
大家好,欢送来到前端研习圈的今日分享。
前言
本系列上期带着大家一起拆解了 Promises/A+ 标准。从 概念,术语,束缚条例
等方面理解了标准
那么本期咱们要做的就是从 标准到实现,并且通过官网的所有测试用例。为了和原生的 Promise 有所区别,咱们就把这一版实现命名为 _Promise
。齐全状态曾经上传到 github,须要的同学自取
提醒:_Promise 仅关注具体实现,不关注成员办法具体应该是公有还是私有等设计细节
源码地址 ->
https://github.com/Mumujianguang/_promise
接下来咱们就进入主题,首先咱们大略整顿一下 todo list
- 定义 Promise 的状态枚举
- 定义 Promise 的类构造
- 实现构造函数
- 实现 then 办法
结构设计
首先,咱们先定义一个枚举对象,将 Promise 的三种状态定义进去
const PromiseState = {
pending: 'pending',
fulfilled: 'fulfilled',
rejected: 'rejected'
}
而后简略设计一下类构造,同时将 state 初始化为 pending 状态
class _Promise {
state = PromiseState.pending;
value;
reason;
fulfilledQueue = [];
rejectedQueue = [];
constructor(executor) {}
resolve(value) {}
reject(reason) {}
then(onFulfilled, onRejected) {}}
因为 then 能够调用屡次,所以咱们设计 fulfilledQueue
和 rejectedQueue
两个数组来别离存储 then 所注册的 胜利的回调 和 失败的回调
为了后续不便外部调用,这里将 resolve 和 reject 两个办法也间接定义在类上
实现
有了根底构造,那么咱们就能够开始着手实现各个办法了。先从 constructor
开始
constructor
回顾一下 Promise
的用法,为了不便解说,咱们把在结构 Promise
实例时传入的函数独自提出来,它的学名叫 executor,接管 resolve
和 reject
两个参数
const executor = (resolve, reject) => {}
const p = new Promise(executor)
看到这儿,置信大家应该都有 constructor
的实现思路了。
但须要留神一点,为了避免 executor 执行时外部报错,须要 try catch 解决一下,并且在 catch 的场景间接将 Promise
reject
掉
constructor(executor) {
try {
executor((value) => this.resolve(value),
(reason) => this.reject(reason)
)
} catch(e) {this.reject(e);
}
}
以上就是 constructor
的实现逻辑,接下来咱们顺着这个思路持续
resolve & reject
在执行 executor 时,咱们将 resolve
和 reject
作为参数传了进去,咱们一起回顾一下它们的作用是什么
- 接管一个 value/reason
- 将
Promise
的状态批改为 胜利 / 失败 - 把 value/reason 作为 胜利 / 失败 回调的参数并依照注册的程序批量执行
- 状态 一旦扭转就不能再被调用
性能点很清晰,那么咱们就能够一条一条去实现它们,间接看代码吧
resolve(value) {if (this.state !== PromiseState.pending) {return;}
this.state = PromiseState.fulfilled;
this.value = value;
this.fulfilledQueue.forEach((onFulfilled) => onFulfilled(value))
}
reject(reason) {if (this.state !== PromiseState.pending) {return;}
this.state = PromiseState.rejected;
this.reason = reason;
this.rejectedQueue.forEach((onRejected) => onRejected(reason))
}
到这里,咱们就实现了 resolve & reject
的实现了,还是比较简单对不对,那么接下来要上强度咯
then
首先咱们思考一下 then
的作用是什么,再同步回顾一下用法
const p = new Promise(resolve => resolve('done'))
p.then(value => console.log(value),
reason => console.log(reason),
)
then
的性能其实很简略,就是单纯注册 胜利 / 失败 的回调,但就是看似如此简略的办法,它的实现难度却是整个 Promise
中最高的。
但不要慌,咱们明天的指标就是要搞懂它,翻过那座山 ( 背地还是山)!
联合上期标准中所讲,咱们先简略概括两点
then
返回的是一个新的 Promise
- 接管 onFulfilled 和 onRejected 作为参数
先写出如下代码
then(onFulfilled, onRejected) {const p2 = new _Promise((resolve, reject) => {// TODO})
return p2;
}
当初 then 的整体框架有了,咱们持续思考,因为 onFulfilled 和 onRejected 的返回值会决定 p2
的状态,那么在注册之前,咱们必定须要对这两个办法做一层 包装
,将新的 Promise 的 resolve 和 reject 的 执行权
与 onFulfilled/onRejected 的返回值关联起来,由返回值的具体情况决定
也就是说 then
的其余逻辑咱们须要写在 新 Promise 的 executor 中。同时还须要留神的是,在 executor 中咱们就须要拜访 p2
,但 p2 是在 executor 执行结束之后才被赋的值,间接拜访必定会报错
then(onFulfilled, onRejected) {const p2 = new _Promise((resolve, reject) => {console.log(p2) // Uncaught ReferenceError: Cannot access 'p2' before initialization
})
return p2;
}
Uncaught ReferenceError: Cannot access ‘p2’ before initialization
如上代码所示,咱们会失去一个与预期统一的报错,怎么躲避呢?其实很容易解决,放到异步工作外面去执行不就好了吗,这里咱们用 queueMicrotask
来实现
then(onFulfilled, onRejected) {const p2 = new _Promise((resolve, reject) => {queueMicrotask(() => {console.log(p2) // _Promise {}})
})
return p2;
}
搞定,当初就能失常拜访 p2
了
那么接下咱们持续依照 标准 的束缚条例给 then
的实现添砖加瓦,先梳理一下大抵要做的事件
- 给 onFulfilled 和 onRejected 增加兼容逻辑(参考条例 2.2.1 & 2.2.7.3 & 2.2.7.4)
- 包装 onFulfilled 和 onRejected,它们的返回值将决定
p2
的 resolve 和 reject 如何执行。因而这里咱们再形象一层 包装器 函数(wrapCallback
) 进去,由这个函数来返回包装后的 onFulfilled(resolveCallback
)和 onRejected(rejectCallback
) - 判断 以后
Promise
的状态是否曾经确定,是的话则间接调用resolveCallback
或rejectCallback
,否则将它们 注册 到各自的回调队列中
梳理结束,就敲代码实现吧
then(onFulfilled, onRejected) {const p2 = new _Promise((resolve, reject) => {queueMicrotask(() => {onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : () => resolve(this.value);
onRejected = typeof onRejected === 'function' ? onRejected : () => reject(this.reason);
const resolveCallback = this.wrapCallback(
p2,
onFulfilled,
resolve,
reject
);
const rejectCallback = this.wrapCallback(
p2,
onRejected,
resolve,
reject
);
if (this.state === PromiseState.fulfilled) {resolveCallback(this.value);
return;
}
if (this.state === PromiseState.rejected) {rejectCallback(this.reason);
return;
}
this.fulfilledQueue.push(resolveCallback)
this.rejectedQueue.push(rejectCallback)
})
})
return p2;
}
至此 then
办法咱们就曾经实现啦,完结撒花!最难的局部也不过如此 …
等等,不太对劲,then
是实现完了,但咱们刚刚又引入了一层 包装器
还没实现呢,还得持续呀各位~
顺便揭示一下大家,还记得上期咱们留的一个大坑吗,没错就是标准中的 束缚条例 2.3
所形容的 Promise Resolution Procedure 流程,但咱们当初不是只剩下 wrapCallback
了吗,所以这个解决流程只能在 wrapCallback
中实现了,为了和标准保持一致,咱们把 Promise Resolution Procedure 流程独自用一个办法来实现,就取名叫 resolutionProcedure
吧
这样一来 wrapCallback
的实现就异样简略了,但留神 then
的回调须要放在 micro task 中去执行,这里咱们还是用 queueMicrotask
来实现;同时还是须要思考回调外部执行报错的场景,所以加上 try catch 来捕捉异样,并在异样的case,触发 reject
基于下面的剖析,咱们就能敲出以下代码了
wrapCallback(promise2, callback, resolve, reject) {return (arg) => queueMicrotask(() => {
let x;
try {x = callback(arg);
} catch (error) {reject(error);
return;
}
this.resolutionProcedure(promise2, x, resolve, reject)
})
}
OK,当初 wrapCallback
实现结束!
终于,咱们来到了最初一个办法 resolutionProcedure
,还记得这个流程是做什么的吗,先抛开其余细节,此流程次要是为了解决 x
是一个 thenable
的场景,以反对第三方实现的 类 promise,满足 互操作性
的要求。
好吧,纠正一下我之前的措辞,在整个 Promise
实现中 resolutionProcedure
才是最难的(手动狗头)
那么接下来咱们还是依据标准 条例 2.3
来梳理出须要实现的逻辑点
- 因为这外面存在 自调用,所以当
p2
与x
是同一个对象时,为了避免死循环,须要间接退出后续解决并触发reject
- 当
x
是一个 Promise 时,则间接将resolve & reject
包装后注册到x
上,由x
的最终状态来决定p2
的状态 - 当
x
是一个一般对象或者办法时,如果x
存在then
办法,则将其视为thenable
。留神,这里须要思考then
是一个 getter 的状况,也就是意味着在 拜访x.then
时也可能会报错,因而获取 then 的过程也须要 try catch 包裹一下,报错的状况间接触发reject
。后续解决的指标和 第 2 点 统一,还是遵循resolve
和reject
只能触发一次的准则,需思考then
执行报错的场景,这里就不做赘述了 - 以上条件均不满足时,则将
x
作为value
触发resolve
接下来就是编码工夫~
resolutionProcedure(promise2, x, resolve, reject) {if (promise2 === x) {reject(new TypeError('promise2 === x'));
return;
}
if (x instanceof _Promise) {
x.then((value) => this.resolutionProcedure(promise2, value, resolve, reject),
(reason) => reject(reason)
);
return;
}
if (
x !== null &&
typeof x === 'object' ||
typeof x === 'function'
) {
let then;
try {then = x.then;} catch (error) {reject(error);
return;
}
if (typeof then === 'function') {
let isCalledResolvePromise = false;
let isCalledRejectPromise = false;
const resolvePromise = (value) => {if (isCalledResolvePromise || isCalledResolvePromise) {return;}
isCalledResolvePromise = true;
this.resolutionProcedure(promise2, value, resolve, reject)
}
const rejectPromise = (reason) => {if (isCalledRejectPromise || isCalledResolvePromise) {return;}
isCalledRejectPromise = true;
reject(reason)
}
try {
then.call(
x,
resolvePromise,
rejectPromise
)
} catch (error) {
if (
!isCalledRejectPromise &&
!isCalledResolvePromise
) {reject(error)
}
}
return;
}
}
resolve(x);
}
至此,咱们的 _Promise
就全副编码结束,那它是否像原生的 Promise
失常工作呢,连忙去测试一下吧!
当然也不必大家去手写测试用例了,官网提供了一个 npm
包promises-aplus-tests
github 地址 -> https://github.com/promises-aplus/promises-tests
依据官网的文档形容,要执行测试用例咱们还须要导出一个规范构造
那么咱们就依照要求导出如下对象
module.exports = {resolved(value) {return new _Promise((resolve) => resolve(value))
},
rejected(reason) {return new _Promise((_, reject) => reject(reason))
},
deferred() {const ret = {};
ret.promise = new _Promise((resolve, reject) => {
ret.resolve = resolve;
ret.reject = reject
})
return ret;
}
}
值得一提的是,deferred
是不是与 Promise.withResolvers
的性能一模一样
回到正题,装置 promises-aplus-tests
后,咱们依据官网文档的测试指南配置一下 测试指令
// package.json
...
"scripts": {"test": "promises-aplus-tests ./core/Promise.cjs"},
...
接下来就能够测试了,在控制台输出
pnpm run test
OK,872 个用例全副通过
写在最初
到这里 Promise 拆解打算
终于实现,这个系列的阶段性指标也达成了,心愿这个系列能真正帮忙到大家,从此不再受 Promise
的毒打~
那么这期就到这里,如果感觉有用的话记得 点赞加关注
哦!
咱们下期见!