大家好,欢送来到前端研习圈的今日分享。
前言
本系列上期带着大家一起拆解了 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
的毒打~
那么这期就到这里,如果感觉有用的话记得点赞加关注
哦!
咱们下期见!