当前位置: 首页 > news >正文

Js 手写 Promise 完整实现

上篇讲了关于 Promise 的各种使用, 从异步编程, 回调地狱, 回调函数的原理实现 (闭包), Promise 的开发约定, 它的基本使用流程, 对象方法, 类方法等, 就从使用上应该问题不大, 但还是想更了解一下其基本实现原理, 本篇就基于 Promise A+ 规范的前提下, 来手写实现 Promise.

声明一下, 以下整体代码和思路均整理来自 b 站大佬 coderwhy 的公开视频

初始设计和构造方法

实现一个叫 CjPromise 的类, 来实现基本的 Pmomise 框架.

补充一下 PromiseA+ 规范: https://promisesaplus.com/

// 基本的结构
class CjPromise {// 初始化
}const promise = new CjPromise((resolve, reject) => {resolve()reject()
})

按照 Prmose 的规范, 在传入 executor 之后会被立即执行, 那直接写在 constructor 里面, 顺带将 resovle, reject 两个函数也定义好

// 初始化 executor, resovle, reject class CjPromise {constructor(executor) {// 跟随 executor 传入回调const resolve = () => {console.log('resolve 被调用')}const reject = () => {console.log('reject 被调用')}executor(resolve, reject)}
}// test
const promise = new CjPromise((resolve, reject) => {resolve() // resolve 被调用reject() //  reject 被调用
})

接着就是要来实现, resolve, reject 不能一起被调用, 而且要确定状态. pending, fulfilled, rejected 会不断产生处理逻辑, 因此最好的方式是将其定义为 常量 来方便识别.

// 状态常量
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'class CjPromise {constructor(executor) {// 最开始是 pending 状态 this.status = PENDING// resolve: 当为 pending,将状态改为 fulfilled, 并将任务安排进回调const resolve = () => {if (this.status === PENDING) {this.status = FULFILLEDconsole.log('resolve 被调用, 安排任务进回调')}}// reject: 当为 pending, 将状态改为 rejected, 并将任务安排进回调 const reject = () => {if (this.status === PENDING) {this.status = REJECTEDconsole.log('reject 被调用, 安排任务进回调')}}executor(resolve, reject)}
}const promise = new CjPromise((resolve, reject) => {// resovle 执行后, 则状态不是 pending, 则 reject() 无效的resolve() reject() 
})

通过状态变量的引入, 并控制状态转换前, 必须要为 pending, 这样就实现 resolve 和 reject 不能一起调用.

const resolve = () => {// 都先检查 pending 后才能动手if (this.status === PENDING) {this.status = FULFILLEDconsole.log('resolve 被调用, 安排任务进回调')}
}

然后是 resolve 和 reject 都是能传值的, 因此在 Promise 内部也要用两个变量 , value reason 将数据存储起来, 因为后面的 then 要进行调用并传递数据的.

constructor(executor) {// 最开始是 pending 状态 this.status = PENDING// 存储 resove, reject 的参数数据this.value = undefinedthis.reason = undefined
}

这个 value 和 reason 的 之后会随着 resovle(value) 和 reject(reason) 而变化, 并存储起来.

// resovle(value), reject(reason)const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'class CjPromise {constructor(executor) {// 最开始是 pending 状态 this.status = PENDING// 存储 resove, reject 的参数数据this.value = undefinedthis.reason = undefined// resolve: 当为 pending,将状态改为 fulfilled, 并将任务安排进回调const resolve = (value) => {if (this.status === PENDING) {this.status = FULFILLED// 将成功的数据存起来, 后面给 then 调用this.value = valueconsole.log('resolve 被调用, 安排任务进回调', value)}}// reject: 当为 pending, 将状态改为 rejected, 并将任务安排进回调 const reject = (reason) => {if (this.status === PENDING) {this.status = REJECTED// 将失败原因存起来, 后面给 catch 调用this.reason = reasonconsole.log('reject 被调用, 安排任务进回调', reason)}}executor(resolve, reject)}
}const promise = new CjPromise((resolve, reject) => {//resolve('okk')reject('err')
})

这样关于 Promise 的基本设计, executor, 状态, reject 和 reject 的传参问题就搞定啦.

then 方法设计

基于上面 resovle() / reject() 方法的调用, 则会出现3个事项:

  • Promise 的状态 由 pending 变为 fulfilled
  • resovle(value) 接收一个 value 或 reason 参数, 也会存给 Promise 对象
  • 通知 then 方法来处理 回调函数, 将 value 或 reason 一并带过来处理
const promise = new CjPromise((resolve, reject) => {resolve('okk')//reject('err')
})promise.then(res => {},err => {}
)

则首先要给 CjPromise 类添加一个叫 then 的对象方法, 并传递2个回调函数, then(onFulfilled, OnRjected)

  • 当 resovle() 被调用, 则执行 then 里面的 onFulfilled() 回调函数
  • 当 reject() 被调用, 则执行 then 里面的 onRejected() 回调函数
  // then 方法 then(onFulfilled, onRejected) {// 保存,上面才能调用this.onFulfilled = onFulfilledthis.onRejected = onRejected}
}const promise = new CjPromise((resolve, reject) => {resolve('okk')//reject('err')
})promise.then(res => {},err => {}
)

这个 then 方法必须先将 onFulfilled 和 onRejected 函数给 Promise 属性存起来, 因为调用是要在 resovle() 或者 rejetet() 函数中去调用的.

但这样还是不行, **分析代码执行顺序, 必须先执行 then() 方法之后, 才会有 onFulfilled / onRejected 的存在, 否知直接在 resove / reject 中调用是找不到的.

可以先在 resove / reject 方法中搞一个 setTimeout() 它会被当做一个宏任务, 不会阻碍主任务进行. 这就实现了可以先 执行 then, 获取到 onFulfilled 和 onReject 方法之后, 再会执行 宏任务里面的 resove / reject 的逻辑.

// resovleconst resolve = (value) => {if (this.status === PENDING) {// 添加宏任务,等主任务执行完了, 才会再执行setTimeout(() => {this.status = FULFILLED// 将成功的数据存起来, 后面给 then 调用this.value = valueconsole.log('resolve 被调用, 安排任务进回调 111-222', value)this.onFulfilled(this.value)}, 0)console.log('111')}// then 方法 then(onFulfilled, onRejected) {// 保存,上面才能调用this.onFulfilled = onFulfilledthis.onRejected = onRejected}// ...
const promise = new CjPromise((resolve, reject) => {resolve('okk')//reject('err')
})promise.then(res => { console.log(res) }, err => { }
)
111
resolve 被调用, 安排任务进回调 111-222 okk
okk

将这个 resovle 包装为宏任务, 这样就会先执行 then 里面的变量定义, 从而让 resovle 中能获取到这个onFulfilled 函数变量了.

但按照 Promise 官方的说法, 这里的 延迟执行 最好用 微任务 会更合适一些, 因此我们可以用 queueMicrotask() 将 resvole 的逻辑添加到 微任务队列 中, 也是更好地实现延迟调用的效果.

// reject: 当为 pending, 将状态改为 rejected, 并将任务安排进回调 
// 通过添加进微任务队列, 会让代码先执行后面的, 再执行这里const reject = (reason) => {if (this.status === PENDING) {this.status = REJECTED // 微任务队列queueMicrotask(() => {// 将失败原因存起来, 后面给 catch 调用this.reason = reasonconsole.log('reject 被调用, 安排任务进回调', reason)this.onRejected(this.reason)})}
}

这里设计到了浏览器事件循环相关的, 先就大致理解这个任务顺序: 主任务 > 微任务队列 > 宏任务队列 即可.

注意: "this.status = REJECTED" 一定要放到 微任务 外面, 先改状态, 否则 reject / resovle 都会被调用了.

相当于是本轮调用, 就直接将 全局状态 确定下来, 这样就不会被修改了.

其实到这里, 最基础版本的 then 方法就已经实现了.

then 方法优化1

但我可以继续再完善一下, es6 的功能, **当 resovle() 的时候, 是可以同时调用多个 then 方法的. **

// then 多次平行调用promise.then(res => { console.log("res:", res) },err => { console.log("err: ", err) }
)promise.then(res => { console.log("res2:", res) },err => { console.log("err2: ", err) }
)
pending 状态
res2: okk

当前的写法, 第二次调的时候, 会覆盖第一次的调用 rosovle / reject , 则最终输出了第二次的结果.

那我们可以弄一个数组, 将每次的 回调函数 都存起来, 到时候遍历数组逐个调用即可.

constructor(executor) {// 最开始是 pending 状态 this.status = PENDING// 存储 resove, reject 的参数数据this.value = undefinedthis.reason = undefined// 存储每次的回调函数this.onFulfilledFns = []this.onRejectedFns = []
}// 对应的 then 方法就先存起来回调函数then(onFulfilled, onRejected) {// 每次调 then 先将回调函数存起来, 后面在一并调用this.onFulfilledFns.push(onFulfilled)this.onRejectedFns.push(onRejected)}

同样的, 在调用 resolve() 的时候, 就进行遍历这个 回调函数组成的数组, 进行挨个调用.

// resovle / reject 中遍历回调函数数组, 执行, 传参const resolve = (value) => {if (this.status === PENDING) {this.status = FULFILLED// 添加进微任务队列,等主任务执行完了, onFulfilled 定义后, 再执行这里queueMicrotask(() => {// 将成功的数据存起来, 后面给 then 调用this.value = value// 遍历回调函数数组, 挨个执行this.onFulfilledFns.forEach(fn => {fn(this.value)})})}
}

这样的话, 上面的并行执行 then 就可以了多次调用了.

const promise = new CjPromise((resolve, reject) => {console.log("pending 状态")resolve('okk')reject('err') // 它不会生效
}) promise.then(res => { console.log("res:", res) },err => { console.log("err: ", err) }
)promise.then(res => { console.log("res2:", res) },err => { console.log("err2: ", err) }
)promise.then(res => { console.log("res3:", res) },err => { console.log("err3: ", err) }
)
res: okk
err:  okkres2: okk
err2:  okkres3: okk
err3:  okk

这样在主任务代码调用是没有问题的, 但如果加一个定时器呢, 理论上来说,此时状态变成了 fulfilled , 也应该被回调的.

// 主任务中, 延迟调用 then, 发现没有被回调setTimeout(() => {promise.then(res => { console.log("res3:", res) },err => { console.log("err3: ", err) })
}, 0);

发现竟然没有回调! 这个应该是不对的, 因为此时的状态已经是 fulfilled 了, 所以还是写的有点问题. 可以看 es6 中是没有问题的.

// es6 只要 resovle 了, 状态确定, 怎么调都是可以的const p = new Promise((resolve, reject) => {resolve('es6 okk')
})setTimeout(() => {// es6-res:  es6 okkp.then(res => console.log("es6-res: ", res))
}, 0);

则, 当 then 调用时, 判断一下当前 promise 状态是 fulfilled 时, 且 onFulfilled 回调函数也传了, 则直接调用;

当状态不确定时, 则将回调函数添加进数组, 等后续 resolve() 或者 reject() 时批量执行

// 状态确定时, 直接调用回调函数; 不确定则添加进回调函数数组then(onFulfilled, onRejected) {// fulfilled 状态再调 then 则直接调用回调函数即可, 注意判空哦if (this.status === FULFILLED && onFulfilled) {onFulfilled(this.value) // then 会传实参的回调函数}// rejected 也是一样处理if (this.status === REJECTED && onRejected) {onRejected(this.reason)}// pending 的时候才添加进来, 否则就上面直接调了// 每次调 then 先将回调函数存起来, 后面在一并调用if (this.status === PENDING) {this.onFulfilledFns.push(onFulfilled)this.onRejectedFns.push(onRejected)}}
}

这里有需要修改的地方, 原来是先改掉状态, 调用放微任务. 而现在则还是将状态变更也放到微任务中.

// 状态变更再放回微任务中const resolve = (value) => {if (this.status === PENDING) {queueMicrotask(() => {// 原来是先该状态再执行;  现在还是改回来this.status = FULFILLEDthis.value = valuethis.onFulfilledFns.forEach(fn => {fn(this.value)})})}
}

这样的优化后, 解决了2个主要的问题:

  • then 方法能进行多次平行调用 (将回调函数存数组, 后面依次调用)
  • 在 Promise 状态确定之下, 再次调用 then , 即便延迟也是可以的 (then 的时候, 状态确定, 直接调用回调)

then 方法优化2

上面为了实现 then 方法能实现多次调用和 Promise 状态确定下, then() 也能直接调用回调, 我们将这个状态变更又放到了微任务队列中, 这其实又会回到之前的问题: resovle() 和 reject() 同时调用, 等主线程执行完后,会同时生效!

// 真是让人头大的状态变更, 放在哪里比较好!
this.status = FULFILLED

这可真是令人头大, 放里面也不合适, 放外面也不合适, 哎呦!!!

状态优化, 一旦确定则不能修改

就还是得修改一下这个微队列, 当微任务执行时, 判断一下, 若状态已经确定, 则停止往后执行. 因为 Prmoise 状态一旦确定了, 就不能让改了.

// 微任务执行时, 若当前状态已定, 则不能再进行修改了const reject = (reason) => {if (this.status === PENDING) {queueMicrotask(() => {// 它会等主任务执行完后, 开始执行这里, 判断若状态已定, 则不能修改if (this.status !== PENDING) return this.status = REJECTEDthis.reason = reasonthis.onRejectedFns.forEach(fn => {fn(this.reason)})})}
}

注意一定要理解这个代码的执行顺序哈, 可能直观看会认为, 这个 微任务队列的外面, 已经进行了

"if (this.status === PENDING)" 的判断, 然后有继续里面 "if (this.status !== PENDING) return " 会有点迷惑, 先让等于 pending 再执行, 执行时又让 不等于 pending 啥的.

这个理解是不正确的, 代码执行顺序不是这样滴! 这里只是写再一起而已, 这里微任务其实要最后才执行

  • 外层要求状态为 pending, 是因为 peding 的时候才能进行 resovle() 或者 reject() 来改变状态
  • 里层微任务时, 判断状态不为 pending (即已确定), 则不让改了, Promise 状态一定确定就永远不让改了.

链式调用优化

继续来做一个大的优化, 关于 Promise 的 then 链式调用实现, 即 then() 方法的返回值, 也是一个 Promise.

// then 链式调用then() {// ....// 要实现链式调用, then() 返回值也是 Promise return new CjPromise((resolve, reject) {resovle(222)})
}// ...
// 链式调用
promise.then((res) => console.log("res1: ", res ),(err) => console.log("res2: ", err ),).then((res) => console.log("res2: ", res), // 222(err) => console.log("err2: ", err))

分析返回的 CjPromise 对象这里, "resolve(222)" , 显然参数不能写死为 222, 然后

**它的值要由上一次的 Promise 状态下的值传递过来的. 上一次的结果, 可能是 resovle('okk'), 或reject("err") . **

关键一步:

((既然 then() 返回的结果要是一个 新的 Promise , 且它的状态会有 then 传入的回调, 那直接将 then 的逻辑, 全部移动到 返回的 新 Prmoise 中即可. **

// then 方法 的逻辑, 直接移动到 返回的 Promise 中then(onFulfilled, onRejected) {// 要实现链式调用, then() 返回值也是 Promise return new CjPromise((resolve, reject) => {// fulfilled 状态再调 then 则直接调用回调函数即可, 注意判空哦if (this.status === FULFILLED && onFulfilled) {onFulfilled(this.value) // then 会传实参的回调函数}if (this.status === REJECTED && onRejected) {onRejected(this.reason)}// pending 的时候才添加进来, 否则就上面直接调了// 每次调 then 先将回调函数存起来, 后面在一并调用if (this.status === PENDING) {this.onFulfilledFns.push(onFulfilled)this.onRejectedFns.push(onRejected)}})}

将原来 then 逻辑的代码放到了返回的 新Prmoise 中, 这样代码也会直接执行的, 因为 executor 会自动调用, 然后它会引用下面的 resove, reject, 又会接着调用 then() , 因此能直接放里面.

接着对 继续对 then() 方法进行改造, **当状态确定时, 在回调 onFulfilled() 或 onRjected() 时, 可以接收返回值, 并将其 resove(value) 或者 reject(reason) 出去, 这样就能给then() 返回的新Promise 调用 自己的 then() 了. **

then(onFulfilled, onRejected) {// 要实现链式调用, then() 返回值也是 Promise return new CjPromise((resolve, reject) => {// fulfilled 状态再调 then 则直接调用回调函数即可, 注意判空哦if (this.status === FULFILLED && onFulfilled) {try {const value = onFulfilled(this.value)resolve(value) // 给新的 Prmoise 的 then 调用} catch (err) {reject(err)}}if (this.status === REJECTED && onRejected) {// 这里是可以拿到回调参数的返回值的, 注意还是会进行 resovle 除非 throw try {const reason = onRejected(this.reason)resolve(reason)} catch (err) {reject(err)}}// ...}

要注意除非是有报错会调用 reject(), 否则都是调用 resovle() 给到返回的新 Promise.

当状态不确定时, 我们当前的方式是将回调函数都用数组存起来, 然后等 resovle() 或者 reject() 的时候去遍历函数并执行.

// 在 then() 的外部去获取 onFulfille 等的结果是不好拿的const resolve = (value) => {if (this.status === PENDING) {queueMicrotask(() => {// ...// 遍历回调函数数组, 挨个执行this.onFulfilledFns.forEach(fn => {// 这里来拿到 value 的话, 是不好进行回调的// 或者可以搞个外部变量存起来, 这样可以, 但很难管理let value = fn(this.value)})})}
}

就在 then() 的外部去获取 onFulfille 等的结果是不好拿的, 那可以考虑从回调函数存入的数组入手, 回到源头来:

// 每次调 then 先将回调函数存起来, 后面再一并调用
if (this.status === PENDING) {this.onFulfilledFns.push(onFulfilled)this.onRejectedFns.push(onRejected)
}

可以将这里是将原回调函数存入数组, 改为 push 一个新的函数, 然后在这个函数里面, 进行回调函数的调用, 并拿到返回值, 并将其 resovle(value) , 这个有点妙呀!

// 每次调 then 先将回调函数存起来, 再包一层, 后面再一并调用if (this.status === PENDING) {this.onFulfilledFns.push(() => {try {const value = onFulfilled(this.value)resolve(value)} catch (err) {reject(err)}})this.onRejectedFns.push(() => {try {const reason = onRejected(this.reason)resolve(reason)} catch (err) {reject(err)}})
}

这样 链式调用的优化就好了, 当前的 then 方法如下:

// then 方法, 要考虑 onFulfilled 或 onRejected 存在空的情况then(onFulfilled, onRejected) {// 要实现链式调用, then() 返回值也是 Promise return new CjPromise((resolve, reject) => {// fulfilled 状态再调 then 则直接调用回调函数即可, 注意判空哦if (this.status === FULFILLED && onFulfilled) {try {const value = onFulfilled(this.value)resolve(value) // 给新的 Prmoise 的 then 调用} catch (err) {reject(err)}}if (this.status === REJECTED && onRejected) {// 这里是可以拿到回调参数的返回值的, 注意还是会进行 resovle 除非 throw try {const reason = onRejected(this.reason)resolve(reason)} catch (err) {reject(err)}}// pending 的时候才添加进来, 否则就上面直接调了// 每次调 then 先将回调函数存起来, 再包一层, 后面再一并调用if (this.status === PENDING) {if (onFulfilled) {this.onFulfilledFns.push(() => {try {const value = onFulfilled(this.value)resolve(value)} catch (err) {reject(err)}})}if (onRejected) {this.onRejectedFns.push(() => {try {const reason = onRejected(this.reason)resolve(reason)} catch (err) {reject(err)}})}}})
}

// then 方法, 要考虑 onFulfilled 或 onRejected 存在空的情况哦, 后面实现 catch() 会用到

还可以将上面的 try-catch 的部分的逻辑再封装我一个工具函数, 逻辑是:

  • 参数有四个: 执行的函数, 给执行函数传递的实参, 另外两个 resovle, reject 函数
  • 执行函数(参数) 一定会返回值, 这个过程进行 try-catch
  • 没有异常就 resolve(value), 有异常则进行 reject(异常)
// 工具函数
function execFnWithCatchErr(exeFn, value, resolve, reject) {try {const value = exeFn(value)resolve(value)} catch (err) {reject(err)}
}

我感觉不优化也行, 适当臃肿, 反而能降低理解的难度吧. 这个 then 方法的优化实现其实是最难的.

// 测试链式调用
const promise = new CjPromise((resolve, reject) => {console.log("pending 状态")resolve(111)reject('err') // 它不会生效
})promise.then(res => { console.log("res:", res) return 222 // 作为返回的新的 Promise 的 resovle 值// throw new Error("err msg");},err => { console.log("err: ", err) }).then(res => { console.log("res2:", res) },err => { console.log("err2: ", err) }
)
pending 状态
res: 111
res2: 222

链式调用, throw -> reject (err) 也都通过测试了. 但还是对 pending 时的 获取 value 或 reason 的处理方式不太那么理解清晰呢.

catch 方法设计

前面实现了 then 的方法, 比较麻烦的地方有关于 状态的精准变化掌控, then 方法的平行多次调用, then 方法返回 Promise 实现链式调用 (这个是最复杂的).

当基本搞定了 then 方法后, 则 catch 方法就是站在巨人的肩膀上, 轻松拿捏它.

// 原来 catch 是作为 then 的第二个回调函数参数
promise.then((res) => {},(err) => {}
)// 现在想要将 catch 写在外面
promise.then((res) => {}).catch((err) => {}
)

可以直接在 catch() 方法中调用 then 即可, 然后第一个参数 onFulfilled 不传即可.

// catch 方法catch(onRejected) {this.then(undefined, onRejected)
}

接着看调用情况, 按照当前的代码逻辑, 将这个 .catch() 写到外面, 则它相当于是调用的 then 返回的新 Promise 的 catch 方法, 而非第一个 Promise 的 catch 结果.

// 这个也不太好弄呀
new CjPromise().catch(err => {})

就现在需求变成了这样:

// promise 中调用 reject
promise1 -> err1 => {} // 因为 then() 又传了 undefined, 导致调不到// 转为给 then返回的 promise2, 加上一个 err 回调
promise2 -> err2 => {} // 希望这个 err2 能被调用// 神之一手就是: 在 promise1 的 err 回调中, 人为报错, 则会自动转到 promise2 的 err 中
promise1 -> err => { throw new Err()}
promise2 -> err =>{} // 这样就自动调用了

这样就直接在 then() 方法中, 在 return new CjPromise() 之前判断一下, 如果它没有值, 则给它一个回调函数, 里面是触发异常.

// 当 onRejected 没有值的时候, 给它回调中抛出异常then(onFulfilled, onRejected) {// onRejected = onRejected || (res => { throw err });// 还是 if 直观一些if (!onRejected) {onRejected = (err => { throw err})}// return new Promise() ...

其实就是当 promise1 进行 reject() 的时候, 通过链式调用的实现规则, 人为抛出异常给到 then() 返回的 新 promise2 中去处理.

这里**要判断 onRejected 没有值的原因是为了实现 "错误透传!" **

当 Promise 被 reject,但在 thencatch 中没有提供相应的错误处理函数(onRejected)时,这个错误应该“穿透”下去,直到被某个 catch 捕获.

**如果, 我们不进行 if (!onRejected) 判断: **

// ❌ 没有错误穿透
const promise = new CjPromise((resolve, reject) => {reject('出错了!'); // Promise 被 reject
});promise.then(res => console.log(res)) // 这里只处理成功,没有处理失败.then(res => console.log('第二个then:', res)) // 这里也只处理成功// 错误应该在这里被捕获.catch(err => console.log('最终捕获:', err)); 

这里执行第一个 then() 时, 此时的 onRejcted 是 undefiend.

当 promise 状态为 rejected 时, 它会调用 this.onRjectedFns 的回调.

由于此时 onRjected 是 undefined, 因此 this.onRjectedFns 并没有函数, 这就调个毛线, 错误就被吞噬了.

后面的 catch 则永远等不到错误, 这个是灾难性的一个骚操作!

来对比一下,

**如果, 加上 onRejected = (err => { throw err}) **:

第一个 then时, onRjected 将会有最初的 undefined , 赋值为 一个回调函数, 里面是一个 抛异常操作

同样当 promise 状态为 rejected 时, 这个函数会被添加到 this.onRjectedFns 数组中,

然后尝试自动从里面取出回调 err => { throw err} 进行调用, 然后它就是立刻报错啦!

继续分析第二个then: (then 返回的 新 Promise)

  • 它监听的是 第一个 then 返回的 Promise
  • 由于第一个 then 返回的 Promise 已经被 reject 了, 因此它的 onRjected == err =>
  • 从而导致第二个 then 返回的 Promise 也被 rejected, 这样就实现了 "错误透传"

最终就会被 catch() 进行捕获, 即 catch(onRejected) 本质上是 then(undefined, onRejected) 的语法糖.

所以这个 catch() 方法的实现代码并不难, 几句话. 但是这个理解过程尤其复杂, 必须要理解 then 才行呢.

至此, 关于 Promise A+ 规范版本的代码其实就已经基本实现啦.

额外补充实现

基于上面 promiseA+ 的实现, 可以看到整个过程有3大难关

  • 状态的一致性管理及回调函数实现, 就涉及到全局状态 和 代码执行顺序的调整 (微任务)
  • then 方法实现细节, 包括多次调用, 返回值也是 promise 的链式调用, 还要考虑状态
  • catch 方法理解难度较大但代码少, 本质是 then(undefined, onRejected) 的语法糖

还要考虑到 Promise 的状态一旦确定则不能改变原则, 还有链式调用中实现错误透传机制, 这个才是精华所在.

finally 对象方法实现

掌握了上述的 then, catch 方法实现过程后, 剩下的这些补充方法, 像这里的 finally 的都是轻松拿捏.

简单说, finally() 的功能就是不论 Promise 的状态是 fulfilled 或者 rejected, 都会执行调用, 本质是就是调用 then() 方法嘛, 因为 catch() 方法也是调 then 方法.

// finally 方法实现finally(onFinally) {// 直接调 then(), 不管啥状态都接收返回 新 Promise 即可// 回调函数没有参数this.then(() => onFinally(), () => onFinally())
}

就理解了则很轻松实现, 但还是有点小问题, 关于 catch() 的, 我们之没有让它返回值

// 当前 不能直接调用 finally, 因为 catch() 没返回值const promise = new CjPromise((resolve, reject) => {console.log("pending 状态")reject('err') 
})promise.then((res) => { console.log("res: ", res) }).catch((err) => { console.log("err: ", err)}).finally(() => console.log('不论啥状态, 都会执行 finally')
)

注意这里不能直接调用 fianlly() 的, 因为 catch() 当前没有返回值, 给 catch() 里面调用的 then() 方法, return 出去即可 , 也是一个 Prmoise

// catch 方法
// 实际调用的是 then() 方法, 返回的是 新的 Promise 
// 加个 rerurn 就有返回值了, 就能调用 finally() 了.catch(onRejected) {// 直接调用 then, 将原来第一个参数 onFulfilled 不传即可return this.then(undefined, onRejected)
}

这样 finally 的基本功能就实现了, 但是, 一旦进行链式调用就又会出现问题:

// 链式调用时, 若开始是 resovle() 则不会执行 then const promise = new CjPromise((resolve, reject) => {console.log("pending 状态")resolve(111)
})promise.then(res => {console.log("res1: ", res)return 'aaa'
}).catch(err => {console.log("err: ", err)
}).finally(() => {console.log("finally") // 不会执行!
})

分析过程:

promise1: then(成功回调, 失败回调-> {throw err})// promise2 有 catch
promise2: then(undefined, 失败回调-> {console.log(err))// 当调用 resolve(111) 时
// 执行  console.log("res1: ", res)// return 'aaa' 则会交给 then 返回的新 Promise 用 resovle() 处理 
// 但是这里此时调用 catch(), 则 新 promise 的 then 的第一个参数是 undefined
// 这样就断层了, 不会执行 finally

解决方案就是当 then(onFulfilled, onRejected), 判断当 onFulfilled 为空时, 也进行错误透传

// then 方法 
then(onFulfilled, onRejected) {if (!onRejected) {onRejected = (err => { throw err})}// 当 onFulfilled 为空, 则也将其进行报错, 透传下去if (!onFulfilled) {onFulfilled = (err => { throw err})}// return new CjPromise()...
}

其实还是考察对这个 then 方法的理解过程.

rosovle 和 reject 类方法实现

这两个静态方法的实现就非常简单了, 就是在这个方法里面, 去返回一个 新的 CjPromise , 然后状态分别对应上这个函数名即可.

// rosolve 类方法static resovle(value) {return new CjPromise((resovle) => resovle(value))
}// reject 类方法 
static reject(reason) {return new CjPromise((undefined, reject) => reject(reason))
}
// 测试类方法
CjPromise.resovle(111).then(res => {console.log("res: ", res) //111
})CjPromise.reject('err').catch(err => {console.log('err: ', err) // err
})

all 和 allSettled 类方法实现

这两个类方法都是接收一个由 Promise 组成的数组, [ promise1, promise2, promise3...], 并返回最终的 Promise

  • all(): 当所有的 promise 都成功时, 终 Promise 状态为 fulfilled, 否则为 rejected, 值为捕捉到的失败那个
  • allSettled(): 等待所有 promise 都由结果后, 状态为 fulfilled, 并用数组返回每个 promise 的状态和结果
// 示范案例// all 都要成功才成功, 返回所有成功结果
const p1 = new Promise((resolve, reject) => {resolve(1)
})const p2 = new Promise((resolve, reject) => {//resolve(2)reject(2)
})const p3 = new Promise((resolve, reject) => {resolve(3)
})Promise.all([p1, p2, p3]).then(res => {console.log('res: ', res) // [1, 2, 3]
}).catch(err => {console.log("err: ", err) // 2
})// allSettled 都要有结果, 然后返回每个结果明细
Promise.allSettled([p1, p2, p3]).then(res => {console.log("res: ", res)
})// res:  [
//   { status: 'fulfilled', value: 1 },
//   { status: 'rejected', reason: 2 },
//   { status: 'fulfilled', value: 3 }
// ]

先来实现 all, 就是遍历 promise 数组, 调 then 去拿结果, 等都拿到了就 resovle, 其中一个错就 reject

// all 类方法实现static all(promiseArr) {return new CjPromise((resolve, reject) => {const values = [] // 存储最终结果的// 遍历, 并存储结果promiseArr.forEach(promise => {promise.then(res => {values.push(res)// 都成功: 当结果数组的长度 等于 传入的 promise 数组长度 则 resovleif (values.length === promiseArr.length) {resolve(values)}}, err => {// 任何一个有错误则 reject reject(err)})})})
}

实现 allSettled, 也是遍历 promise 数组, 调 then 去拿到每个的状态和结果, 最终一并返回.

// allSettled 类方法实现static allSettled(promiseArr) {return new CjPromise((resovle, reject) => {const ret = []promiseArr.forEach(promise => {promise.then(res => {ret.push({ status: 'fulfilled', value: res })// 若结果数组长度 等于 传入的 promiseArr 长度, 则说明都拿到结果了if (ret.length === promiseArr.length) {resovle(ret)}},err => {ret.push({ status: 'rejected', reason: err })// 也是根据结果数组长度来判断是否都拿到结果, resovle 出去if (ret.length === promiseArr.length) {resovle(ret)}})})})
}

race 和 any 类方法实现

这俩类方法也是接收一个 Promise 组成的数组, 并返回一个最终结果的 Promise:

  • race(): 谁先返回结果就算谁, 不论成功或失败
  • any(): 至少有一个成功则 resovle, 否则就是 reject

实现 race(), 遍历 promise 数组去拿结果, 一旦拿到就回调

// race 类方法
static race(promiseArr) {return new CjPromise((resovle, reject) => {promiseArr.forEach(promise => {promise.then(res => {resovle(res)}, res => {reject(res)})})})
}

可以再简写一下, 因为都是有结果就直接回调, 不论 reject 还是 resovle:

// race 类方法-优化static race(promiseArr) {return new CjPromise((resovle, reject) => {promiseArr.forEach(promise => {// 不论啥结果, 直接回调promise.then(resovle, reject)
})
})
}

实现 any(), 遍历 promise 数组去拿结果, 只要有一个成功的, resovle 否则 reject

// any 类方法static any(promiseArr) {const reasons =[]return new CjPromise((resovle, reject) => {promiseArr.forEach(promise => {promise.then(resovle, err => {// 将错误都收集起来reasons.push(err)// 若错误个数 等于 promiseArr 长度, 则全错, 要 reject 了if (reasons.length === promiseArr) {reject(new AggregateError(reasons))}})})})}

整体 promise 实现

// 状态常量
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'class CjPromise {constructor(executor) {this.status = PENDINGthis.value = undefinedthis.reason = undefined// 存储每次的回调函数this.onFulfilledFns = []this.onRejectedFns = []// resolve: 当为 pending,将状态改为 fulfilled, 并将任务安排进回调const resolve = (value) => {if (this.status === PENDING) {queueMicrotask(() => {if (this.status !== PENDING) returnthis.status = FULFILLEDthis.value = valuethis.onFulfilledFns.forEach(fn => {fn(this.value)})})}}// reject: 当为 pending, 将状态改为 rejected, 并将任务安排进回调 const reject = (reason) => {if (this.status === PENDING) {queueMicrotask(() => {if (this.status !== PENDING) returnthis.status = REJECTEDthis.reason = reasonthis.onRejectedFns.forEach(fn => {fn(this.reason)})})}}executor(resolve, reject)}// then 方法 then(onFulfilled, onRejected) {if (!onRejected) {onRejected = (err => { throw err})}// 当 onFulfilled 为空, 则也将其进行报错, 透传下去if (!onFulfilled) {onFulfilled = (err => { throw err})}// 要实现链式调用, then() 返回值也是 Promise return new CjPromise((resolve, reject) => {if (this.status === FULFILLED && onFulfilled) {try {const value = onFulfilled(this.value)resolve(value) // 给新的 Prmoise 的 then 调用} catch (err) {reject(err)}}if (this.status === REJECTED && onRejected) {// 这里是可以拿到回调参数的返回值的, 注意还是会进行 resovle 除非 throw try {const reason = onRejected(this.reason)resolve(reason)} catch (err) {reject(err)}}// pending 的时候才添加进来, 否则就上面直接调了// 每次调 then 先将回调函数存起来, 再包一层, 后面再一并调用if (this.status === PENDING) {if (onFulfilled) {this.onFulfilledFns.push(() => {try {const value = onFulfilled(this.value)resolve(value)} catch (err) {reject(err)}})}if (onRejected) {this.onRejectedFns.push(() => {try {const reason = onRejected(this.reason)resolve(reason)} catch (err) {reject(err)}})}}})}// catch 方法catch(onRejected) {// 直接调用 then, 将原来第一个参数 onFulfilled 不传即可return this.then(undefined, onRejected)}// finally 方法 finally(onFinally) {// 直接调 then(), 不管啥状态都接收返回 新 Promise 即可this.then(() => onFinally(),() => onFinally())}// rosolve 类方法static resovle(value) {return new CjPromise((resovle) => resovle(value))}// reject 类方法 static reject(reason) {return new CjPromise((undefined, reject) => reject(reason))}// all 类方法static all(promiseArr) {return new CjPromise((resolve, reject) => {const values = [] // 存储最终结果的// 遍历, 并存储结果promiseArr.forEach(promise => {promise.then(res => {values.push(res)// 都成功: 当结果数组的长度 等于 传入的 promise 数组长度 则 resovleif (values.length === promiseArr.length) {resolve(values)}}, err => {// 任何一个有错误则 reject reject(err)})})})}// allSettled 类方法static allSettled(promiseArr) {return new CjPromise((resovle, reject) => {const ret = []promiseArr.forEach(promise => {promise.then(res => {ret.push({ status: 'fulfilled', value: res })// 若结果数组长度 等于 传入的 promiseArr 长度, 则说明都拿到结果了if (ret.length === promiseArr.length) {resovle(ret)}},err => {ret.push({ status: 'rejected', reason: err })// 也是根据结果数组长度来判断是否都拿到结果, resovle 出去if (ret.length === promiseArr.length) {resovle(ret)}})})})}// race 类方法static race(promiseArr) {return new CjPromise((resovle, reject) => {promiseArr.forEach(promise => {// 不论啥结果, 直接回调promise.then(resovle, reject)})})}// any 类方法static any(promiseArr) {const reasons =[]return new CjPromise((resovle, reject) => {promiseArr.forEach(promise => {promise.then(resovle, err => {// 将错误都收集起来reasons.push(err)// 若错误个数 等于 promiseArr 长度, 则全错, 要 reject 了if (reasons.length === promiseArr) {reject(new AggregateError(reasons))}})})})}
}

至此, 关于 Promise 的实现过程就差不多导致了, 核心流程都通过自己手写一遍的话, 相信在后续使用层面则再无任何心智负担.

http://www.sczhlp.com/news/52870/

相关文章:

  • 网站开发语言排行榜网站改版应该怎么做
  • 用ip的网站要备案吗网络优化软件哪个好
  • 电子商务网站建设首页流程啥是深圳网站定制开发
  • 凡科网站源码下载php 手机网站 模板
  • 内心的冲动和精力的矛盾
  • 20220719_QQ群_KCS1_OAEP模式RSA解密
  • 优化网站做内链接WordPress动画随音乐变化
  • 可以做网站首页的图片素材北京装修公司全包价格
  • 有什么网站可以做试题dedecms公司网站怎么做
  • 正规的国内网站建设公司好的平面设计灵感网站
  • 免费php企业网站wordpress干什么用的
  • 服务器网站建设教程视频教程怎样在网上卖东西步骤
  • metro风格网站模板网站备案信息加到哪里
  • 荣成网站制作公司网站建设的必要性分析
  • 外贸网站外链平台模板网站开发注意事项
  • 怎么导入视频到wordpress北京优化seo排名优化
  • 石家庄手机网站制作网站开发简单
  • AI中的潜意识学习:大语言模型隐藏的安全隐患
  • GAS_Aura-Initializing Vital Attributes
  • 去图书馆的意义
  • 交友应用数据泄露事件分析
  • seo站长之家WordPress博客Modown模板
  • 手机制作网站app网站域名注册哪家好
  • 公司网站平台的作用互联网创业项目网站
  • 招聘网站哪个好自己做的网站图片打开慢
  • 滨海做网站的全国文明城市创建标准
  • 黑群晖建设个人网站wordpress数字超市
  • 教学网站开发代码重庆 网站开发
  • 做电影网站 需要进那些群高埗做网站公司
  • 用jsp做的网站源代码网站建设iis