🖊️ 手写Promise
⚡️ 状态和数据
当Promise对象进入已决阶段时,表示异步操作结束,而该Promise对象的状态和数据代表了异步操作结果。我们可以在类中定义成员属性status
和result
,分别用来表示当前Promise对象的状态和数据。而这两个成员是不能被外部直接获取的,因此需要将它们私有化。
Promise对象的状态总共有三种,并且它们的字符串表示都是不变的,即常量。因此,我们可以在类中将这三种状态定义成静态私有属性。
class Promise {
static #PENDING = 'pending'
static #FULFILLED = 'fulfilled'
static #REJECTED = 'rejected'
#status
#result
}
🔄 resolve与reject
resolve
与reject
两者都是用来改变当前Promise对象状态和数据的函数。在JavaScript中,普通函数中的this
值是由该函数被调用时才确定的,而resolve
和reject
两者通常会提供给外部使用。因此,我们不能确定函数体中的this
值的具体指向。
==那如何才能保证resolve和reject函数中的this值一定为当前Promise对象呢?==其实,我们可以使用箭头函数,箭头函数中的this
值为定义时最近的外层上下文中的this
值。因此,我们可以将resolve
和reject
以箭头函数的形式定义成私有成员方法。
==那为什么要将resolve和reject函数私有化呢?==resolve
和reject
不能直接被外部使用,它们会在Promise实例化、executor
执行时被当作实参传入。
resolve
和reject
两者的逻辑类似,都是当当前Promise对象的状态为挂起时,改变其状态和数据。因此,我们可以将这个逻辑封装成一个函数。需要注意的是,封装的函数可以是普通函数,因为其只在resolve
和reject
中被调用,this
值是可以确定的。
class Promise {
static #PENDING = 'pending'
static #FULFILLED = 'fulfilled'
static #REJECTED = 'rejected'
#status
#result
#resolve = (data) => {
this.#change(Promise.#FULFILLED, data)
}
#reject = (reason) => {
this.#change(Promise.#REJECTED, reason)
}
#change(status, result) {
if (this.#status !== Promise.#PENDING) return
this.#status = status
this.#result = result;
}
}
🔨 构造器
Promise构造器接受一个executor
函数,该函数会在Promise实例化时被同步执行,且会将resolve
和reject
两者作为实参传入。但此过程仍需注意以下几点。
当构造器接受的
executor
不是函数类型时,会抛出一个TypeError
错误。因此,我们可以写一个辅助函数来判断某个变量是否为函数类型。在JavaScript中,函数类型的判断有多种方式,这里选择typeof
关键字。executor
调用前,需将当前Promise实例的状态初始化为挂起。若
executor
调用过程中发生错误,则拒绝当前Promise对象,原因为错误对象。
class Promise {
static #PENDING = 'pending'
static #FULFILLED = 'fulfilled'
static #REJECTED = 'rejected'
#status
#result
constructor(executor) {
if (!this.#isFunction(executor)) {
throw new TypeError('executor is not a function')
}
this.#status = Promise.#PENDING
try {
executor(this.#resolve, this.#reject)
} catch (error) {
this.#reject(error)
}
}
#isFunction(func) {
return typeof func === 'function'
}
#resolve = (data) => {
this.#change(Promise.#FULFILLED, data)
}
#reject = (reason) => {
this.#change(Promise.#REJECTED, reason)
}
#change(status, result) {
if (this.#status !== Promise.#PENDING) return
this.#status = status
this.#result = result
}
}
⛓️ then
then
方法接受两个可选参数,一个是 onFulfilled
(成功时的回调函数),一个是 onRejected
(失败时的回调函数)。当Promise对象进入已决阶段时,其会根据自身状态,将关联的数据作为实参执行相应的回调。同时,then
方法会返回一个全新的Promise对象,新Promise对象的状态和数据取决于回调的执行。
==那可以在then方法中立即执行回调处理吗?==答案是不可以的,因为Promise对象的已决是可以发生在异步任务中的,例如定时器。在调用then
方法时,当前Promise对象可能仍为挂起状态。
==那如何保证传入then方法的回调函数的执行时机是正确的呢?==我们可以使then
方法不立刻执行相应的回调,而是将onFulfilled
、onRejected
、新Promise对象的resolve
、新Promise对象的reject
四者作为对象的方法记录至一个数组handlers
中。同时,将回调执行与新Promise对象的状态数据变更封装成一个函数#run
,在then
和#change
这两个方法的逻辑最后进行调用。
Q&A:
- ==为什么 handlers是一个数组?==
答:一个Promise对象可以多次注册回调函数。
- ==⭐️为什么需要记录新Promise对象的resolve和reject?==
答:新Promise对象的状态数据取决于后续任务的处理,即回调的执行。而状态和数据的变更必须依赖
resolve
与reject
。因此在记录回调外,还需额外记录它们。
- ==⭐️为什么#run方法需要被调用两次?==
答:若Promise对象的已决是同步的,则
#run
首先会在#change
中被调用,而此时then
未被调用,#handlers
为空;若Promise对象的已决是异步的,则#run
首先会在then
中被调用,而此时#change
未被调用,Promise对象的状态为挂起。因此,保证#run
方法调用两次,并把以上这两种特殊情况排除,就能实现#run
的真正逻辑只处理一次,且能够确保回调执行时机是正确的。在#run
逻辑开头,可以使用以下代码作条件来排除这两种特殊情况。
==确定了#run方法的调用时机,那其核心逻辑是什么呢?==其实,#run
方法的任务非常简单与明确:执行回调,确定新Promise对象的状态和数据。
新Promise对象的状态更改与回调函数的处理有关,而并不关心回调函数是用于处理成功还是失败。因此,我们可以继续封装一个#runCallback
函数,而在#run
方法中仅需选择onFulfilled
、onRejected
中的一个作为回调即可。此时,#run
方法的核心逻辑将转移至#runCallback
中了。
==那核心逻辑(新Promise对象的状态更改规则)究竟是什么呢?==具体来说,有以下几点。
传入的实参回调不是函数,则新Promise对象的状态和数据与前一个Promise对象保持一致。
后续任务处理成功,即回调执行无错误。新Promise对象的状态和数据取决于回调的返回值,以下是两种情况。
回调的返回值也是一个任务(Promise对象),则新Promise对象的状态和数据与回调返回的Promise对象保持一致。
回调的返回值不是一个任务(Promise对象),则新Promise对象的状态为已兑现,数据为返回值。
后续任务处理失败,即回调执行抛出错误,则新Promise对象的状态为已拒绝,数据为异常对象。
class Promise {
static #PENDING = 'pending'
static #FULFILLED = 'fulfilled'
static #REJECTED = 'rejected'
#status
#result
#handlers
constructor(executor) {
if (!this.#isFunction(executor)) {
throw new TypeError('executor is not a function')
}
this.#status = Promise.#PENDING
this.#handlers = []
try {
executor(this.#resolve, this.#reject)
} catch (error) {
this.#reject(error)
}
}
#isFunction(func) {
return typeof func === 'function'
}
#resolve = (data) => {
this.#change(Promise.#FULFILLED, data)
}
#reject = (reason) => {
this.#change(Promise.#REJECTED, reason)
}
#change(status, result) {
if (this.#status !== Promise.#PENDING) return
this.#status = status
this.#result = result
this.#run()
}
then(onFulfilled, onRejected) {
return new Promise((resolve, reject) => {
this.#handlers.push({onFulfilled, onRejected, resolve, reject})
this.#run()
})
}
#run() {
// ⭐难点①
if (this.#handlers.length === 0 || this.#status === Promise.#PENDING) return
while (this.#handlers.length) {
const {onFulfilled, onRejected, resolve, reject} = this.#handlers.shift()
if (this.#status === Promise.#FULFILLED) {
this.#runCallback(onFulfilled, resolve, reject)
} else {
this.#runCallback(onRejected, resolve, reject)
}
}
}
#runCallback(callback, resolve, reject) {
if (!this.#isFunction(callback)) {
const settled = this.#status === Promise.#FULFILLED ? resolve : reject
settled(this.#result)
return
}
try {
const result = callback(this.#result)
if (result instanceof Promise) {
// ⭐难点②
result.then(resolve, reject)
} else {
resolve(result)
}
} catch (error) {
reject(error)
}
}
}
tip:
then
方法是Promise最核心的设计,其实现共有两个难点。
- ⭐回调的调用时机。解决方式上文已讲述。
- ⭐当回调成功执行且返回一个Promise对象后,新Promise对象的状态数据应与回调返回的Promise对象保持一致。解决方式为:调用返回Promise对象的
then
方法,并将新Promise对象的resolve
、reject
分别注册为成功回调和失败回调。
🛠️ 微任务
Promise的回调为微任务。实际上,截至目前为止,ECMAScript标准中并没有微任务的概念。微任务是JS任务调度的实现机制,目前很多主流的JS运行环境都采用这种机制来实现事件循环,例如大多数现代浏览器、Node.js。
需要注意的是,不同的 JavaScript 运行环境或引擎实现微任务的方式是有差异的。在现代浏览器环境中,我们可以利用MutationObserver API
来创建微任务;在Node.js环境中,我们可以利用process.nextTick()
来创建微任务。
tip: 在Node.js环境中,任务队列是分种类的,并有各自的优先级。微任务队列分为
nextTick队列
与Promise队列
。需要注意的是,这个Promise是Node.js中内置的构造函数,并非我们手写的Promise。而nextTick队列
的执行优先级高于Promise队列
,因此,我们只是借助了nextTick
模拟了一个微任务。
class Promise {
static #PENDING = 'pending'
static #FULFILLED = 'fulfilled'
static #REJECTED = 'rejected'
#status
#result
#handlers
constructor(executor) {
if (!this.#isFunction(executor)) {
throw new TypeError('executor is not a function')
}
this.#status = Promise.#PENDING
this.#handlers = []
try {
executor(this.#resolve, this.#reject)
} catch (error) {
this.#reject(error)
}
}
#isFunction(func) {
return typeof func === 'function'
}
#resolve = (data) => {
this.#change(Promise.#FULFILLED, data)
}
#reject = (reason) => {
this.#change(Promise.#REJECTED, reason)
}
#change(status, result) {
if (this.#status !== Promise.#PENDING) return
this.#status = status
this.#result = result
this.#run()
}
then(onFulfilled, onRejected) {
return new Promise((resolve, reject) => {
this.#handlers.push({onFulfilled, onRejected, resolve, reject})
this.#run()
})
}
#run() {
if (this.#handlers.length === 0 || this.#status === Promise.#PENDING) return
while (this.#handlers.length) {
const {onFulfilled, onRejected, resolve, reject} = this.#handlers.shift()
if (this.#status === Promise.#FULFILLED) {
this.#runCallback(onFulfilled, resolve, reject)
} else {
this.#runCallback(onRejected, resolve, reject)
}
}
}
#runCallback(callback, resolve, reject) {
this.#runMicroTask(() => {
if (!this.#isFunction(callback)) {
const settled = this.#status === Promise.#FULFILLED ? resolve : reject
settled(this.#result)
return
}
try {
const result = callback(this.#result)
if (result instanceof Promise) {
result.then(resolve, reject)
} else {
resolve(result)
}
} catch (error) {
reject(error)
}
})
}
#runMicroTask(task) {
if (!this.#isFunction(task)) throw new Error("task is not a function")
if (typeof MutationObserver === "function" &&
typeof MutationObserver.prototype.observe === "function") {
const observer = new MutationObserver(task)
const textNode = document.createTextNode("origin")
observer.observe(textNode, {
characterData: true
})
textNode.data = 'change'
} else if (typeof process === 'object' && typeof process.nextTick === "function") {
process.nextTick(task)
} else (
setTimeout(task, 0)
)
}
}
📜 Promise A+规范
Promise A+规范是一个社区规范,定义了Promise的行为、方法和交互方式,以确保在不同的JavaScript环境中Promise的一致性和可互操作性。
一致性与可互操作性是指符合Promise A+规范的Promise对象可以与其他符合同样规范的Promise对象进行无缝交互和组合,而不仅限于JavaScript运行环境内置的Promise。
综上所述,我们不能直接通过result instanceof Promise
来判断回调返回值是否是一个任务对象,而应该封装一个函数来判断返回值的类型是否符合Promise A+规范,以下为判断条件。
- 类型为
function
或object
。 - 类型需实现Thenable,即实例存在一个
then
方法。
tip:
async/await
也支持与其他遵循 Promise A+ 规范的类型进行交互。async
虽然支持兼容交互,但是它永远返回环境内置的Promise类型的对象。
class Promise {
static #PENDING = 'pending'
static #FULFILLED = 'fulfilled'
static #REJECTED = 'rejected'
#status
#result
#handlers
constructor(executor) {
if (!this.#isFunction(executor)) {
throw new TypeError('executor is not a function')
}
this.#status = Promise.#PENDING
this.#handlers = []
try {
executor(this.#resolve, this.#reject)
} catch (error) {
this.#reject(error)
}
}
#isFunction(func) {
return typeof func === 'function'
}
#resolve = (data) => {
this.#change(Promise.#FULFILLED, data)
}
#reject = (reason) => {
this.#change(Promise.#REJECTED, reason)
}
#change(status, result) {
if (this.#status !== Promise.#PENDING) return
this.#status = status
this.#result = result
this.#run()
}
then(onFulfilled, onRejected) {
return new Promise((resolve, reject) => {
this.#handlers.push({onFulfilled, onRejected, resolve, reject})
this.#run()
})
}
#run() {
if (this.#handlers.length === 0 || this.#status === Promise.#PENDING) return
while (this.#handlers.length) {
const {onFulfilled, onRejected, resolve, reject} = this.#handlers.shift()
if (this.#status === Promise.#FULFILLED) {
this.#runCallback(onFulfilled, resolve, reject)
} else {
this.#runCallback(onRejected, resolve, reject)
}
}
}
#runCallback(callback, resolve, reject) {
this.#runMicroTask(() => {
if (!this.#isFunction(callback)) {
const settled = this.#status === Promise.#FULFILLED ? resolve : reject
settled(this.#result)
return
}
try {
const result = callback(this.#result)
if (this.#isPromiseAPlus(result)) {
result.then(resolve, reject)
} else {
resolve(result)
}
} catch (error) {
reject(error)
}
})
}
#runMicroTask(task) {
if (!this.#isFunction(task)) throw new Error("task is not a function")
if (typeof MutationObserver === "function" &&
typeof MutationObserver.prototype.observe === "function") {
const observer = new MutationObserver(task)
const textNode = document.createTextNode("origin")
observer.observe(textNode, {
characterData: true
})
textNode.data = 'change'
} else if (typeof process === 'object' && typeof process.nextTick === "function") {
process.nextTick(task)
} else (
setTimeout(task, 0)
)
}
#isPromiseAPlus(value) {
return !(value == null || (typeof value !== 'object'
&& typeof value !== 'function') || typeof value.then !== 'function')
}
}
🔌 静态API
Promise.resolve
Promise.resolve
用于返回一个已兑现的Promise对象,数据为给定的第一个参数。此过程有以下注意点。
- 若参数类型为内置的Promise,则直接返回
- 若参数类型不是内置的Promise,但符合Promise A+规范,则返回与其状态数据保持一致的nei'zh
reject
all
any
allSettled
race