Promise - 源码分析 <附带Typescript>
前言
去年也整过,但是由于去年功力尚浅,整了一半整不下去了,感觉有些弯弯绕绕的很复杂,菜是原罪。。。然后今天想自己从源头开始自己试试,看会是什么情况,结果试着敲了敲,发现还可以,能用!!然后就从网上扒了份源码下来研究。
首先我们先研究下 Promise
的正经写法。
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('1秒后打印该文本')
}, 1000)
})
p1.then((res) => {
console.log({ res })
})
超低配版 Promise
这里我们先按照自己的视角来完成上述的 Promise
功能。
从上述代码观感来说,等 1 秒后 resolve
会触发 .then
方法,以前的我一直弄不懂怎么让 resolve
通知 .then
方法触发。其实这里可以当成一种 观察者模式 的运用。我上篇文章有介绍 Redux
源码,里面就用到了这个模式,所以我们带入 Promise
就会发现, p1.then
就好比 subscribe
,而 resolve
就好比 dispatch
。
所以我们可以简单写出下面的代码。
class MyPromise {
thenCb = undefined
constructor(callback) {
callback(this.resolve.bind(this))
}
resolve(res) {
this.thenCb(res)
}
then(cb) {
this.thenCb = cb
}
}
是不是很简单,仅仅几行代码就实现了 Promise
(doge),当然这里的代码经不起推敲,仅仅只是为了实现功能而实现的。
那么接下来还是让我们从源码抓起,看看大佬们当年的脑回路~
Promise 源码分析
大概读了下源码,我这里按照个人感觉大概分析下几点:
-
根据
Promise
使用规则,我们再new MyPromise
的时候,它的构造函数内需要主动执行一次传递给它的callback
函数。(执行函数是为了能够改造resolve
,reject
) -
then
函数调用时,需要将我们thenCallback
存储起来,且为了链式调用,我们需要主动在这里new MyPromise
一次,并返回新的MyPromise
实例。【注】在创建新实例时,传递一个空函数作为其callback
,这时候就不需要主动执行空函数了 -
紧接着,由于
callback
的执行,最终会执行到resolve
或reject
(下面都以resolve
情形说明) -
由于,
resolve
是我们类内部写好的,所以我们可以在这里创建一个 微任务 ,里面去执行我们存储起来的thenCallback
,执行完成之后为了 链式调用 ,需要再执行一次新的MyPromise
实例的resolve
让它接着触发新的then
这样就实现了,Promise 常规用法 和 链式调用
下面按照这些去敲,就不进行源码分析了,主要是思路 ,细节咱没必要和源码一致蛤。
// 每个 Promise 都有用下面的状态枚举标识。
// 当调用 resolve 时,状态会从 PENDING 变成 FULFILLED
// 当调用 reject 时,状态会从 PENDING 变成 REJECTED
enum STATUS_ENUM {
PENDING = 0,
FULFILLED,
REJECTED,
}
type ResolveCallback = (value: unknown) => void
type RejectCallback = (reson?: unknown) => void
type PromiseCallback = (resolve: ResolveCallback, rejcet: RejectCallback) => void
// 空函数,用于链式调用时
const noop = () => {}
// 保存 .then .catch 和 promise实例 的类
class Handler {
constructor(
public thenCallback: ResolveCallback | undefined,
public catchCallback: RejectCallback | undefined,
public promise: MyPromise,
) {}
}
class MyPromise {
status: STATUS_ENUM = STATUS_ENUM.PENDING
deferreds: Handler | null = null
constructor(callback: PromiseCallback) {
// 空函数为链式调用的 promise 实例,不需要继续向下处理,所以退出
if (callback == noop) return
this.doCallback(callback)
}
/**
* 处理 resolve reject 方法
*
* @param callback
*/
doCallback(callback: PromiseCallback) {
let done = false
const _resolve = (_value: unknown) => {
if (done) return
done = true
this.resolve(_value)
}
const _reject = (_value: unknown) => {
if (done) return
done = true
this.reject(_value)
}
callback(_resolve, _reject)
}
// 调用 then 方法时,将 then 的回调存储起来
then(thenCallback?: ResolveCallback, catchCallback?: RejectCallback) {
// 为了链式调用,需要创建新的 Promise实例,存储起来并且在后面返回
const nextPromise = new MyPromise(noop)
// 这里存储 then 的回调,且存储了 下一个promise 的实例
// 是为了在执行完 resolve 之后,可以触发 下一个promise 的 resolve ,使得链式调用成功
this.deferreds = new Handler(thenCallback, catchCallback, nextPromise)
// 返回下一次promise的实例,使得我们可以进行链式调用
return nextPromise
}
// catch 方法,实际上也可以看成是调用了 then(undefined, catchCallback)
catch(catchCallback: RejectCallback) {
return this.then(undefined, catchCallback)
}
resolve(value: unknown) {
this.status = STATUS_ENUM.FULFILLED
this.do(value)
}
reject(reson: unknown) {
this.status = STATUS_ENUM.REJECTED
this.do(reson)
}
do(value: unknown) {
// Promise 源码内部用的是 aspa 创建的微任务,然后这里涉及到了JS底层
// 我们这里就只使用 setTimeout 来模拟
setTimeout(() => {
const cb = this.status == STATUS_ENUM.FULFILLED ? this.deferreds?.thenCallback : this.deferreds?.catchCallback
// catch 必备:
// 如果是调用 p1.then().catch() 且 p1 内部触发的是reject,那么由于 then 里没有放 catchCallback 所以 cb 会是 undefined
// 这时候,就继续向下找,直到找到有 callback 的那个
if (!cb) {
if (this.status == STATUS_ENUM.FULFILLED) {
this.deferreds?.promise.resolve(value)
} else {
this.deferreds?.promise.reject(value)
}
return
}
// cb 存在,则正常调用,利用 trycatch 判断下一步的promise要触发哪个
try {
const nextRes = cb!(value)
this.deferreds?.promise.resolve(nextRes)
} catch (error) {
this.deferreds?.promise.reject(error)
}
})
}
}
试着按照自己扒的那几点做了下,简单用 TS 写了一下,感觉大差不差,不过实际源码内部还有做一些处理,比如他还维护了一个 deferredsStatus
,不过我这里就没做这些了,感觉一个状态就够了,也没细究其具体原因,大概思路理解下就好了。
顺便还在后面把 catch
的也一起弄了,整体代码看下来难度不是很大,小巧而精美。简直 nice~
转载自:https://juejin.cn/post/7239514481755668517