参考 Promise/A+ 规范和测试用例手写 Promise

前言
这可能是手写promise较清晰的文章之一。
由浅至深逐步分析了原生测试用例,以及相关Promise/A+规范。阅读上推荐以疑问章节为切入重点,对比Promise/A+规范与ECMAScript规范的内在区别与联系,确定怎样构建异步任务和创建promise实例。然后开始手写章节,过程中代码与测试可参考 promise-coding 仓库。
也试着回答以下关键问题。
- 什么是广义对象?
- 如何检验
promise类型? promise与thenable两类型有何区别?
疑问
如果不太清楚Promise,建议参考《ECMAScript 6 入门》,预习下Promise相关用法知识。
除此之外,对规范也要有大致认识,我们将根据几个疑问来细致阐述。
什么是 Promise/A+ 规范?
Promise有多种社区规范,例如 Promise/A、Promise/B、Promise/D 和 Promises/KISS 等。
Promise/A+ 在Promise/A之上,规范了术语并拓展了参数行为,省略了一些有问题的部分。
Promise/A+有很多实现,例如第三方库 q、when 和 bluebird 等。实际上任何Promise通过测试,都被认为是对Promise/A+规范的一种实现。
Promise/A+规范官方测试用例为 promises-aplus-tests
原生 Promise 实现了 Promise/A+?
在Promise/A+规范 The ECMAScript Specification 章节中。
The ECMAScript Specification
...
Largely due to the actions of the Promises/A+ community, the Promise global specified by ECMAScript and present in any conforming JavaScript engine is indeed a Promises/A+ implementation!
叙述了JavaScript引擎中的Promise也是对Promise/A+规范的一种实现。
为什么呢?
Promise/A+规范内容上仅说明了Promise状态和then方法。
ECMAScript规范不仅规定Promise为构造函数,还添加了静态方法,例如Promise.resolve、Promise.all和Promise.race等,新增了原型方法Promise.prototype.catch和Promise.prototype.finally等。其中Promise.prototype.then相关内容,则是根据Promise/A+规范转化而来。
我们知道,JavaScript就是对ECMAScript规范的一种实现,而ECMAScript规范中Promise.prototype.then相关内容又继承了Promise/A+规范。
那么可以说,JavaScript引擎中的Promise,即原生Promise,就是对Promise/A+规范的一种实现。
如何构建异步任务?
Promise/A+规范规定then方法接收两个参数。
promise.then(onFulfilled, onRejected)
在 2.2.4 小结中明确了参数必须以异步形式运行。
2.2.4. onFulfilled or onRejected must not be called until the execution context stack contains only platform code.
注解 3.1 补充可用宏任务setTimeout和setImmediate,或者微任务MutationObserver(浏览器环境)和process.nextTick(node环境)达到异步。
3.1. ...In practice, this requirement ensures that onFulfilled and onRejected execute asynchronously, after the event loop turn in which then is called, and with a fresh stack. This can be implemented with either a "macro-task" mechanism such as setTimeout or setImmediate, or with a "micro-task" mechanism such as MutationObserver or process.nextTick.
综上所述,Promise/A+规范仅规定了参数以异步形式运行,并未规定是宏任务还是微任务。
注意
V8引擎内部为微任务,考虑一致性推荐 queueMicrotask 创建微任务,兼容性相对较好
如何创建 promise?
Promise/A+规范并未提及如何创建promise。
ECMAScript6规范发布以来,多数是以构造函数方式创建promise。
new Promise(executor)
实际上在此之前,还流行一种Deferred方式,例如 JQuery.Deferred。
$.Deferred()
我们以定时器为例,对比下两者差异。
// ECMAScript6 Promise
const promise = new Promise(resolve => {setTimeout(() => {resolve(1)}, 1000)
})promise.then(v => {console.log(v) // 1
})// JQuery Deferred
const deferred = $.Deferred()deferred.promise().then(v => {console.log(v) // 1
})setTimeout(() => {deferred.resolve(1)
}, 1000)
你也注意到了吧,Deferred方式相对更加灵活,可以在任何时机修改状态。而Promise方式自由度减少了很多,不仅代码层级多了一层,而且只能在函数参数中修改状态。
可能你会问,那为什么TC39放弃了Deferred,而决定了Promise构造器方式呢?
Deferred方式存在一个较严重的缺陷,即在业务流程阶段,不容易捕获异常。
const deferred = $.Deferred()deferred.promise().catch(v => {console.log(v)
});(function () {throw new Error() // Uncaught Errordeferred.resolve(1)
})()
如果想让promise捕获异常,业务代码可修改为。
;(function () {try {throw new Error()} catch (error) {deferred.reject(error)}deferred.resolve(1)
})()
而Promise构造器方式则非常容易。
const promise = new Promise(resolve => {throw new Error()resolve(1)
})promise.catch(v => {console.log(v) // Error
})
两相比较下ECMAScript6确定了以构造器方式创建promise。
个人认为
Deferred更多是一个发布订阅器,而Promise则相对更加强大,是一个异步流程解决方案,ECMAScript6规范将其独立为一个模块是相当合理的
手写
Promise/A+更多地是规范了算法逻辑,并未规定语法层面的实现方式。
我们可以参考原生Promise语法,并结合简单用例,手写以符合Promise/A+规范。
注意
Promise/A+规范相关内容将特别标注
实例初始属性
原生创建Promise实例。
new Promise(() => {})
// {
// [[PromiseState]]: 'pending',
// [[PromiseResult]]: undefined,
// }
相关特征包括。
Promise为构造函数- 默认状态为
pending,默认结果为undefined - 三种状态分别为等待态
pending、解决态fulfilled和拒绝态rejected——「Promise/A+ 2.1」
代码编写如下,其中属性[[PromiseState]]用于保存状态,[[PromiseResult]]用于保存结果。
const PromiseState = '[[PromiseState]]'
const PromiseResult = '[[PromiseResult]]'const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'class Promise {[PromiseState] = PENDING;[PromiseResult] = undefined
}
ES2020规范 proposal-class-fields 允许实例属性定义在类内部的最顶层,相对更加清晰简洁
executor 执行器
原生Promise传参函数。
new Promise(function executor() {console.log(1) // 1
})
console.log(2) // 2new Promise((resolve, reject) => {resolve(3)
})
// {
// [[PromiseState]]: 'fulfilled',
// [[PromiseResult]]: 3,
// }new Promise((resolve, reject) => {reject(4)
})
// {
// [[PromiseState]]: 'rejected',
// [[PromiseResult]]: 4,
// }
相关特征包括。
- 实例创建过程中参数
executor将同步执行 - 执行器
executor包括resolve和reject两个函数参数,resolve执行实例状态修改为解决态,reject执行实例状态修改为拒绝态
以下为优化代码,注意私有方法用箭头函数,可将内部this指向实例对象。
class Promise {...#resolve = value => {this[PromiseState] = FULFILLEDthis[PromiseResult] = value}#reject = reason => {this[PromiseState] = REJECTEDthis[PromiseResult] = reason}constructor(executor) {executor(this.#resolve, this.#reject)}
}
ES2020规范 proposal-class-fields 允许实例定义私有属性或方法,仅可在类内部使用,外部无法使用
状态不可变性
原生Promise修改状态。
new Promise((resolve, reject) => {resolve(1)resolve(2)
})
// {
// [[PromiseState]]: 'fulfilled',
// [[PromiseResult]]: 1,
// }new Promise((resolve, reject) => {resolve(3)reject(4)
})
// {
// [[PromiseState]]: 'fulfilled',
// [[PromiseResult]]: 3,
// }
相关特征包括。
- 处于解决态或者拒绝态,一定不能再修改为任何状态——「Promise/A+ 2.1.2 / 2.1.3」
- 处于等待态的时候,可能变为解决态或者拒绝态——「Promise/A+ 2.1.1」
代码优化为。
#resolve = value => {if (this[PromiseState] === PENDING) {this[PromiseState] = FULFILLEDthis[PromiseResult] = value}
}
#reject = reason => {if (this[PromiseState] === PENDING) {this[PromiseState] = REJECTEDthis[PromiseResult] = reason}
}
方法传参
原生Promise上then方法传参。
const p = new Promise((resolve, reject) => {resolve()
})p.then(undefined, undefined)
相关特征包括。
promise必须有then方法,且接收两个参数onFulfilled和onRejected——「Promise/A+ 2.2」onFulfilled和onRejected都是可选参数,若不是函数,必须被忽略——「Promise/A+ 2.2.1」onFulfilled和onRejected一定被作为普通函数调用——「Promise/A+ 2.2.5」
代码修改为。
class Promise {...then(onFulfilled, onRejected) {onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : () => {}onRejected = typeof onRejected === 'function' ? onRejected : () => {}}
}
参数为非函数时,为保证可被调用,暂时返回普通函数
then 方法
原生Promise执行then方法。
const p1 = new Promise((resolve, reject) => {resolve(1)
})
p1.then(v => {console.log(v) // 1
})const p2 = new Promise((resolve, reject) => {reject(2)
})
p2.then(undefined, v => {console.log(v) // 2
})
相关特征包括。
- 如果
onFulfilled或onRejected是一个函数,必须在promise被解决或被拒绝后调用,且promise值或原因作为第一个参数——「Promise/A+ 2.2.2 / 2.2.3」
代码修改为。
then(onFulfilled, onRejected) {...if (this[PromiseState] === FULFILLED) {onFulfilled(this[PromiseResult])}if (this[PromiseState] === REJECTED) {onRejected(this[PromiseResult])}
}
异步 executor
目前代码并未完全符合「Promise/A+ 2.2.2 / 2.2.3」规范,例如executor为异步情况时,还会存在一些问题。
const p = new Promise((resolve, reject) => {setTimeout(() => {resolve(1)}, 1000)
})p.then(v => {console.log(v)
})
控制台没有打印任何内容
为什么呢?
实例p在创建完成后,还处在等待态。紧接着执行then方法,then方法内部没有等待态相关逻辑,也就没有任何操作。1s后resolve执行,也仅仅是将p状态修改为解决态。
如何解决呢?
可以在等待态就保存onFulfilled和onRejected函数,在resolve修改状态时再执行。
代码优化为。
class Promise {...#onFulfilledCallBack = undefined#onRejectedCallBack = undefined#resolve = value => {if (this[PromiseState] === PENDING) {...this.#onFulfilledCallBack?.(this[PromiseResult])}}#reject = reason => {if (this[PromiseState] === PENDING) {...this.#onRejectedCallBack?.(this[PromiseResult])}}...then(onFulfilled, onRejected) {...if (this[PromiseState] === PENDING) {this.#onFulfilledCallBack = onFulfilledthis.#onRejectedCallBack = onRejected}}
}
?.为ES2020规范中 proposal-optional-chaining 可选链操作符
多次调用 then
原生Promise多次调用then方法。
const p = new Promise((resolve, reject) => {setTimeout(() => {resolve()}, 1000)
})p.then(() => {console.log(1) // 1
})p.then(() => {console.log(2) // 2
})p.then(() => {console.log(3) // 3
})
相关特征包括。
then方法函数参数按语法顺序执行- 同一
promise上then方法可能被多次调用——「Promise/A+ 2.2.6」
代码优化如下,注意为了保证顺序,两数组内函数都是先进先出。
class Promise {...#onFulfilledCallBacks = []#onRejectedCallBacks = []#resolve = value => {if (this[PromiseState] === PENDING) {...while (this.#onFulfilledCallBacks.length) {this.#onFulfilledCallBacks.shift()(this[PromiseResult])}}}#reject = reason => {if (this[PromiseState] === PENDING) {...while (this.#onRejectedCallBacks.length) {this.#onRejectedCallBacks.shift()(this[PromiseResult])}}}...then(onFulfilled, onRejected) {...if (this[PromiseState] === PENDING) {this.#onFulfilledCallBacks.push(onFulfilled)this.#onRejectedCallBacks.push(onRejected)}}
}
返回 promise
原生Promise返回值。
const p = new Promise(() => {})p.then()
// {
// [[PromiseState]]: 'pending',
// [[PromiseResult]]: undefined,
// }
相关特征包括。
then方法必须返回一个新promise——「Promise/A+ 2.2.7」
代码暂时修改为。
then(onFulfilled, onRejected) {...if (this[PromiseState] === PENDING) {...}const promise = new Promise(() => {})return promise
}
函数参数返回值
原生Promise函数参数onFulfilled返回数值。
const p = new Promise((resolve, reject) => {resolve()
})p.then(() => {return 1
})
// {
// [[PromiseState]]: 'fulfilled',
// [[PromiseResult]]: 1,
// }
相关特征包括。
- 如果
onFulfilled或onRejected返回一个值x,运行promise解决程序——「Promise/A+ 2.2.7.1」 - 如果
x不是一个对象或函数,用x解决promise——「Promise/A+ 2.3.4」
何为promise解决程序呢?
「Promise/A+ 2.3」叙述是一个抽象操作,可表示为[[Resolve]](promise, x)。其中主要根据x类型,决定新promise的状态和结果。
比如x不是一个对象或函数,例如数值,则新promise状态将确定为解决态,结果为x,即用x解决promise。
// {
// [[PromiseState]]: 'fulfilled',
// [[PromiseResult]]: x,
// }
那么如何在onFulfilled或onRejected返回数值x时,又修改新promise状态和结果呢?
then(onFulfilled, onRejected) {...if (this[PromiseState] === FULFILLED) {const x = onFulfilled(this[PromiseResult])}...const promise = new Promise(() => {})return promise
}
你可能想到了。
then(onFulfilled, onRejected) {...const promise = new Promise(() => {})if (this[PromiseState] === FULFILLED) {const x = onFulfilled(this[PromiseResult])promise.#resolve(x)}...return promise
}
可修改状态也符合规范,但个人认为此方式存在一些缺陷。
将实例属性resolve私有化,就是为了限制外部访问。以promise.#resolve访问,而非this.#resolve,已经处于外部访问的范畴了,思路上不是很合理。
还有更好的办法吗?
我们知道在executor执行器上,resolve和reject两个参数也可修改状态。
如果将if语句体迁移至executor内部,有没有可能呢?答案是可以的。
then(onFulfilled, onRejected) {...const promise = new Promise((resolve, reject) => {if (this[PromiseState] === FULFILLED) {const x = onFulfilled(this[PromiseResult])resolve(x)}...})return promise
}
if语句体在executor外部时,同步执行。在executor内部时,也是同步执行
相关特征完全实现了吗?并没有。
若executor为异步情况时,还存在一些问题。
const p1 = new Promise((resolve, reject) => {setTimeout(() => {resolve()}, 1000)
})const p2 = p1.then(() => {return 2
})setTimeout(() => {console.log(p2)// {// [[PromiseState]]: 'pending',// [[PromiseResult]]: undefined,// }
}, 2000)
控制台打印内容与原生不一致
为什么呢?
实例p1处于等待态,执行then方法将onFulfilled保存至数组中。1s后resolve执行,p1状态修改为解决态,紧接着取出运行onFulfilled,p2状态无任何变化。
我们可以在onFulfilled执行时,对返回值x稍加处理。
const promise = new Promise((resolve, reject) => {...if (this[PromiseState] === PENDING) {this.#onFulfilledCallBacks.push(() => {const x = onFulfilled(this[PromiseResult])resolve(x)})...}
})
处理函数
为了统一处理不同类型x值,并严格实现规范「Promise/A+ 2.3」中各子章节。
修改代码并创建resolvePromise函数,参数暂时为x和resolve。
class Promise {...then(onFulfilled, onRejected) {...const promise = new Promise((resolve, reject) => {if (this[PromiseState] === FULFILLED) {const x = onFulfilled(this[PromiseResult])resolvePromise(x, resolve)}...if (this[PromiseState] === PENDING) {this.#onFulfilledCallBacks.push(() => {const x = onFulfilled(this[PromiseResult])resolvePromise(x, resolve)})...}})return promise}
}function resolvePromise(x, resolve) {resolve(x)
}
研读部分子章节。
2.3.1. If promise and x refer to the same object, reject promise with a TypeError as the reason.
2.3.2.2. If/when x is fulfilled, fulfill promise with the same value.
2.3.2.3. If/when x is rejected, reject promise with the same reason.
可确认参数promise和x、resolve、reject。
const promise = new Promise((resolve, reject) => {if (this[PromiseState] === FULFILLED) {const x = onFulfilled(this[PromiseResult])resolvePromise(promise, x, resolve, reject)}...if (this[PromiseState] === PENDING) {this.#onFulfilledCallBacks.push(() => {const x = onFulfilled(this[PromiseResult])resolvePromise(promise, x, resolve, reject)})...}
})function resolvePromise(promise, x, resolve, reject) {resolve(x)
}
抛出异常
原生Promise抛出异常。
const p = new Promise((resolve, reject) => {resolve()
})p.then(() => {throw new Error()
}).then(undefined, v => {console.log(v) // Error
})
相关特征包括。
- 如果
onFulfilled或onRejected抛出一个异常e,新promise为拒绝态且原因为e——「Promise/A+ 2.2.7.2」
代码优化为。
const promise = new Promise((resolve, reject) => {if (this[PromiseState] === FULFILLED) {try {const x = onFulfilled(this[PromiseResult])resolvePromise(promise, x, resolve, reject)} catch (e) {reject(e)}}...
})
类似地executor为异步情况时,也存在一些问题。
const p = new Promise((resolve, reject) => {setTimeout(() => {resolve()}, 1000)
})p.then(() => {throw new Error() // Uncaught Error
}).then(undefined, v => {console.log(v)
})
未捕获到异常
为什么呢?
实例p处于等待态,执行then方法将onFulfilled保存。1s后resolve执行,p状态修改为解决态,紧接着取出onFulfilled,运行内部抛出了异常。
代码优化为。
const promise = new Promise((resolve, reject) => {...if (this[PromiseState] === PENDING) {this.#onFulfilledCallBacks.push(() => {try {const x = onFulfilled(this[PromiseResult])resolvePromise(promise, x, resolve, reject)} catch (e) {reject(e)}})...}
})
异步任务
原生Promise异步。
const p = new Promise((resolve, reject) => {resolve(1)
})console.log(2) // 2
p.then(v => {console.log(v) // 1
})
console.log(3) // 3
注意打印顺序
2 3 1
相关特征包括。
onFulfilled或onRejected必须以异步形式运行——「Promise/A+ 2.2.4」
代码简单修改为。
const queueTask = queueMicrotaskclass Promise {...then() {const promise = new Promise((resolve, reject) => {if (this[PromiseState] === FULFILLED) {try {queueTask(() => {const x = onFulfilled(this[PromiseResult])resolvePromise(promise, x, resolve, reject)})} catch (e) {reject(e)}}...})return promise}
}
注意try...catch并不能捕获到异步函数内抛出的异常,例如。
try {setTimeout(() => {throw new Error() // Uncaught Error})
} catch (error) {console.log(error)
}
那如何优化呢?
我们可以将全部try...catch语句放到异步函数中。
const promise = new Promise((resolve, reject) => {if (this[PromiseState] === FULFILLED) {queueTask(() => {try {const x = onFulfilled(this[PromiseResult])resolvePromise(promise, x, resolve, reject)} catch (e) {reject(e)}})}...
})
类似地executor为异步情况时,也存在一些问题。
const p = new Promise(resolve => {setTimeout(() => {console.log(1) // 1resolve(2)console.log(3) // 3}, 1000)
})p.then(v => {console.log(v) // 2
})
打印顺序
1 2 3(原生打印顺序1 3 2)
为什么呢?
onFulfilled没有以异步形式运行。
代码修改为。
const promise = new Promise((resolve, reject) => {...if (this[PromiseState] === PENDING) {this.#onFulfilledCallBacks.push(() => {queueTask(() => {try {const x = onFulfilled(this[PromiseResult])resolvePromise(promise, x, resolve, reject)} catch (e) {reject(e)}})})...}
})
合并重复代码。
const promise = new Promise((resolve, reject) => {const resolved = () => {queueTask(() => {try {const x = onFulfilled(this[PromiseResult])resolvePromise(promise, x, resolve, reject)} catch (e) {reject(e)}})}const rejected = () => {queueTask(() => {try {const x = onRejected(this[PromiseResult])resolvePromise(promise, x, resolve, reject)} catch (e) {reject(e)}})}if (this[PromiseState] === FULFILLED) {resolved()}if (this[PromiseState] === REJECTED) {rejected()}if (this[PromiseState] === PENDING) {this.#onFulfilledCallBacks.push(resolved)this.#onRejectedCallBacks.push(rejected)}
})
参数优化
原生Promise值穿透。
const p1 = new Promise((resolve, reject) => {resolve(1)
})
p1.then(undefined)
// {
// [[PromiseState]]: 'fulfilled',
// [[PromiseResult]]: 1,
// }const p2 = new Promise((resolve, reject) => {reject(2)
})
p2.then(undefined, undefined)
// {
// [[PromiseState]]: 'rejected',
// [[PromiseResult]]: 2,
// }
相关特征包括。
- 如果
onFulfilled不是一个函数且原promise被解决,新promise必须也被解决,且值与原promise相同——「Promise/A+ 2.2.7.3」 - 如果
onRejected不是一个函数且原promise被拒绝,新promise必须也被拒绝,且原因与原promise相同——「Promise/A+ 2.2.7.4」
代码优化如下。
then(onFulfilled, onRejected) {onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => valueonRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason }...
}
注意
throw抛出异常将被try...catch捕获,进而拒绝新promise
类型
如何处理不同类型x呢?
还是参考规范「Promise/A+ 2.3」各子章节,以优化resolvePromise处理函数。
循环引用
原生Promise循环引用。
const p1 = new Promise((resolve, reject) => {resolve()
})const p2 = p1.then(() => {return p2
})
// {
// [[PromiseState]]: 'rejected',
// [[PromiseResult]]: TypeError: Chaining cycle detected for promise #<Promise>,
// }
相关特征包括。
- 如果
promise和x引用同一对象,则拒绝promise,原因为一个TypeError——「Promise/A+ 2.3.1」
代码优化为。
function resolvePromise(promise, x, resolve, reject) {if (promise === x) {return reject(new TypeError('Chaining cycle detected for promise #<Promise>'))}resolve(x)
}
传递性
原生Promise函数参数onFulfilled返回promise。
const p1 = new Promise((resolve, reject) => {resolve()
})
const p2 = new Promise((resolve, reject) => {reject(1)
})p1.then(() => {return p2
})
// {
// [[PromiseState]]: 'rejected',
// [[PromiseResult]]: 1,
// }
相关特征包括。
- 如果
x是等待态,promise必须保持等待态,直到x被解决或被拒绝——「Promise/A+ 2.3.2.1」 - 如果
x是解决态,用相同的值解决promise——「Promise/A+ 2.3.2.2」 - 如果
x是拒绝态,用相同的原因拒绝promise——「Promise/A+ 2.3.2.3」
也就是promise状态与x始终都保持一致。
可能会存在x初始为等待态,然后又转变为解决态或拒绝态。过程中两者状态始终一致,若x状态转变,promise状态也将转变。
那如何知道x状态转变呢?答案就是then方法。
x.then(onFulfilled, onRejected)
x转变为解决态时将运行onFulfilled,转变为拒绝态时将运行onRejected。
那我们就可在onFulfilled或onRejected内部去修改promise状态。
代码优化为。
function resolvePromise(promise, x, resolve, reject) {...if (x instanceof Promise) {x.then(value => {resolve(value)}, reason => {reject(reason)})} else {resolve(x)}
}
简化为。
function resolvePromise(promise, x, resolve, reject) {...if (x instanceof Promise) {x.then(resolve, reject)} else {resolve(x)}
}
广义对象
何为广义对象呢?
能添加属性或方法的变量,都称之为广义上的对象,例如数组、函数等。
创建isObject工具函数,更多参考 lodash.isObject。
function isObject(value) {const type = typeof valuereturn value !== null && (type === 'object' || type === 'function')
}
然后阅读规范「Promise/A+ 2.3.3」小节,省略部分暂时不考虑。
- 如果
x是一个对象或函数(广义对象)- 让
then为x.then - 如果获取
x.then导致抛出了一个异常e,用e作为原因拒绝promise - 如果
then是一个函数,用x作为this调用它,且包含两个参数,分别为resolvePromise和rejectPromise- 如果
resolvePromise用一个值y调用,运行[[Resolve]](promise, y) - 如果
rejectPromise用一个原因r调用,用r拒绝promise ...- 如果调用
then抛出了一个异常e...- 否则用
e作为作为原因拒绝promise
- 如果
- 如果
then不是一个函数,用x解决promise
- 让
转译为代码。
function resolvePromise(promise, x, resolve, reject) {...if (x instanceof Promise) {...} else {if (isObject(x)) {var thentry {then = x.then} catch (e) {reject(e)}if (typeof then === 'function') {try {then.call(x,y => {resolvePromise(promise, y, resolve, reject)},r => {reject(r)})} catch (e) {reject(e)}} else {resolve(x)}} else {resolve(x)}}
}
规范中运行[[Resolve]](promise, y),即递归resolvePromise,为什么呢?
原因在于y值可能还是promise或者广义对象等等。
我们来看一个原生Promise示例。
const p = new Promise(resolve => {resolve()
})
const thenable1 = {then(reslove) {reslove(1)},
}
const thenable2 = {then(resolve) {resolve(thenable1)},
}p.then(() => {return thenable2
})
// {
// [[PromiseState]]: 'fulfilled',
// [[PromiseResult]]: 1,
// }
优先级
以下为刚才省略的部分。
- 如果
then是一个函数.........- 如果
resolvePromise和rejectPromise都被调用,或者对其中一个多次调用,那么第一次调用优先,以后的调用都会被忽略 - 如果调用
then抛出了...- 如果
resolvePromise或rejectPromise已经被调用,则忽略它 ...
- 如果
为了限制哪种情况呢?
还是来看一个原生Promise示例。
const p = new Promise(resolve => {resolve()
})
const thenable1 = {then(reslove) {setTimeout(() => {reslove(2)}, 0)},
}
const thenable2 = {then(resolve) {resolve(thenable1)resolve(1)},
}p.then(() => {return thenable2
})
// {
// [[PromiseState]]: 'fulfilled',
// [[PromiseResult]]: 2,
// }
代码如何优化呢?
我们可定义布尔变量called,标记是否运行参数resolvePromise或rejectPromise。然后在第一次运行时将called修改为true,而以后的都会return被忽略。
if (typeof then === 'function') {var called = falsetry {then.call(x,y => {if (called) returncalled = trueresolvePromise(promise, y, resolve, reject)},r => {if (called) returncalled = truereject(r)})} catch (e) {if (called) returncalled = truereject(e)}
}
thenable
规范「Promise/A+ 1.1」小结陈述了。
promise是一个对象或函数(广义对象),存在then方法且行为符合规范。
第三方Promise库、原生Promise以及我们手写版本Promise,创建的promise实例,其实都是标准的promise类型。
而代码中x instanceof Promise语句,检验是否为promise类型,就有问题了。例如x被第三方库创建,也是标准promise类型,但是并不会运行if语句体,而是错误地运行else语句体。
function resolvePromise(promise, x, resolve, reject) {...if (x instanceof Promise) {...} else {...}
}
还有方法可确定x为promise类型吗?答案是没有。
怎么办呢?
既然无法检验promise类型,那就退而求其次,检验类似promise类型的,即鸭式辩型。
鸭子类型(
duck typing),也叫鸭式辩型,一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子
规范「Promise/A+ 1.2」提出了thenable类型,即定义了then方法的对象或函数。
{then() {...},
}
thenable是promise的鸭子类型
检验是否为promise类型,则降级为检验是否为thenable类型。
代码修改为。
function resolvePromise(promise, x, resolve, reject) {if (promise === x) {return reject(new TypeError('Chaining cycle detected for promise #<Promise>'))}if (isObject(x)) {var thentry {then = x.then} catch (e) {reject(e)}if (typeof then === 'function') {var called = falsetry {then.call(x,y => {if (called) returncalled = trueresolvePromise(promise, y, resolve, reject)},r => {if (called) returncalled = truereject(r)})} catch (e) {if (called) returncalled = truereject(e)}} else {resolve(x)}} else {resolve(x)}
}
测试
安装官方测试用例 promises-aplus-tests。
npm i promises-aplus-tests -D
promise代码中新增以下。
// promise.js
class Promise {...
}Promise.deferred = () => {const result = {}result.promise = new Promise((resolve, reject) => {result.resolve = resolveresult.reject = reject})return result
}module.exports = Promise
新增测试命令。
// package.json
{..."scripts": {"test": "promises-aplus-tests promise.js"},..."devDependencies": {"promises-aplus-tests": "^2.1.2"}
}
运行npm run test。

小结
全文共计两万五千字有余,参考Promise/A+规范手写了then方法和promise解决程序。
相关代码可参考 promise-coding 仓库,支持node和浏览器环境测试。
如何手写Promise到此就结束了。
扩展
学有余力或意犹未尽的伙伴们。
贴出两个代码片段,可在原生Promise与手写Promise环境下运行。
// 1
new Promise(resolve => {resolve(Promise.resolve())
}).then(() => {console.log(3)
})Promise.resolve().then(() => {console.log(1)}).then(() => {console.log(2)}).then(() => {console.log(4)})// 2
Promise.resolve().then(() => {console.log(0)return Promise.resolve()}).then(() => {console.log(4)})Promise.resolve().then(() => {console.log(1)}).then(() => {console.log(2)}).then(() => {console.log(3)}).then(() => {console.log(5)}).then(() => {console.log(6)})
看看能否分析出两者之间的细微差异?答案是不能。
更多请持续关注更文,或在参考链接中探索一二。
参考
- Promise/A+ 规范译文
- 原生 Promise 和手写 Promise 的区别是什么?
- resolve 时序
- V8 源码解读 Promise
🎉 写在最后
🍻伙伴们,如果你已经看到了这里,觉得这篇文章有帮助到你的话不妨点赞👍或 Star ✨支持一下哦!
手动码字,如有错误,欢迎在评论区指正💬~
你的支持就是我更新的最大动力💪~
GitHub / Gitee、GitHub Pages、掘金、CSDN 同步更新,欢迎关注😉~
相关文章:
参考 Promise/A+ 规范和测试用例手写 Promise
前言 这可能是手写promise较清晰的文章之一。 由浅至深逐步分析了原生测试用例,以及相关Promise/A规范。阅读上推荐以疑问章节为切入重点,对比Promise/A规范与ECMAScript规范的内在区别与联系,确定怎样构建异步任务和创建promise实例。然后开…...
yolov5数据集制作
yolov5 数据集的格式 每个图像的标注信息存储在一个独立的txt文件中每个txt文件的名称应该与其对应的图像名称相同,只是文件扩展名不同。例如: 对于名为“image1.jpg”的图像,其标注信息应存储在名为“image1.txt”的txt文件中。 在每个txt文件中,每一行表示一个对象的标注…...
主板EC程序烧写异常致无法点亮修复经验
主板型号:Gigabyte AB350M-Gaming3 官网上明确写着支持R5 5500,但按照如下步骤实践下来实际是不支持的 升级biosF31到F40版本的注意事项: 步骤: 1 使用Q-Flash先将bios升级到f31版本;2 然后下载提示中的ECFW Update To…...
【Java爬取赛事网站】命令行输出(仅供学习)
Java爬取赛事网站 Java爬取赛事网站Java爬取赛事网站参与社区的问题回答Gitcode项目地址PSP表格解题思路描述问题接口设计和实现过程编写中的测试关键代码展示性能改进单元测试异常处理心路历程与收获参与社区的问题回答 问题回答这个作业属于哪个课程软件工程-23年春季学期这…...
redis主从复制原理
在 Redis 中,我们可以通过 SLAVEOF 命令或者 slaveof 选项,让一个服务器去复制另一个服务器,被复制的服务器称为“主服务器”,发起复制的服务器称为“从服务器”,由两种服务器组成的模式称为“主从复制”。 主从复制原…...
buu刷题(第一周)
目录 [DDCTF 2019]homebrew event loop action:trigger_event%23;action:buy;5%23action:get_flag; [CISCN2019 华东南赛区]Web4 [RootersCTF2019]babyWeb [GWCTF 2019]mypassword [NESTCTF 2019]Love Math 2 [BSidesCF 2019]Pick Tac Toe [RootersCTF2019]ImgXweb [SW…...
算法训练营 day62 单调栈 每日温度 下一个更大元素 I
算法训练营 day62 单调栈 每日温度 下一个更大元素 I 每日温度 739. 每日温度 - 力扣(LeetCode) 给定一个整数数组 temperatures ,表示每天的温度,返回一个数组 answer ,其中 answer[i] 是指对于第 i 天,…...
ChIP-seq 分析:Peak 注释与可视化(9)
1. 基因注释 到目前为止,我们一直在处理对应于转录因子结合的 ChIPseq 峰。顾名思义,转录因子可以影响其靶基因的表达。 转录因子的目标很难单独从 ChIPseq 数据中确定,因此我们通常会通过一组简单的规则来注释基因的峰: 如果峰与…...
ABB机器人配置DeviceNet总线IO板以及信号分配的具体方法示例
ABB机器人配置DeviceNet总线IO板以及信号分配的具体方法示例 基本步骤: 配置IO板分配IO信号这里以DeviceNet总线的DSQC652为例进行说明: 配置IO板的基本步骤: 配置IO板的型号 连接到总线 配置IO板的地址 (1台机器人可以配置多个IO板连接到DeviceNet总线,为了让机…...
2023 年网络安全漏洞的主要原因
网络安全漏洞已经并将继续成为企业面临的主要问题。因此,对于企业领导者来说,了解这些违规行为的原因至关重要,这样他们才能更好地保护他们的数据。 在这篇博文中,我们将概述 2023 年比较普遍的网络安全漏洞的主要原因。 云…...
剑指 Offer 34. 二叉树中和为某一值的路径
剑指 Offer 34. 二叉树中和为某一值的路径 难度:middle\color{orange}{middle}middle 题目描述 给你二叉树的根节点 rootrootroot 和一个整数目标和 targetSumtargetSumtargetSum ,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。 叶子节…...
2023前端vue面试题(边面边更)
Vue中key的作用 vue 中 key 值的作用可以分为两种情况来考虑: 第一种情况是 v-if 中使用 key。由于 Vue 会尽可能高效地渲染元素,通常会复用已有元素而不是从头开始渲染。因此当使用 v-if 来实现元素切换的时候,如果切换前后含有相同类型的…...
webpack配置完全指南
前言 对于入门选手来讲,webpack 配置项很多很重,如何快速配置一个可用于线上环境的 webpack 就是一件值得思考的事情。其实熟悉 webpack 之后会发现很简单,基础的配置可以分为以下几个方面: entry 、 output 、 mode 、 resolve …...
juju创建lxd容器时如何使用本地镜像(by quqi99)
作者:张华 发表于:2023-03-01 版权声明:可以任意转载,转载时请务必以超链接形式标明文章原始出处和作者信息及本版权声明 问题 没有外网,所以配置了一个local custom镜像库,也使用了container-image-meta…...
后端程序员学习前端开发之第一步环境搭建
一、安装 Node.js Node.js 是一个开源的、跨平台的 JavaScript 运行时环境。Node.js官网 二、安装 npm 镜像 因为 npm 是国外的,所以使用起来速度比较慢。我们这里使用了淘宝的 cnpm 镜像安装 vue。使用淘宝的 cnpm 命令管理工具代替默认的 npm 管理工具。 进入c…...
【记录问题】RuntimeError:working outside of application context. Flask使用SQLAlchemy数据库
前提:Flask使用SQLAlchemy数据库 本质:依赖包版本不匹配 问题1:报错RuntimeError:working outside of application context. 运行程序报错,如下错误: 原因:flask-sqlalchemy 版本过高导致&am…...
自动化测试难点案例分析,其实自动化你用错方向还不如不用
随着国内企业软件开发及测试水平的提升,许多企业开始尝试开展自动化测试的应用,以提高测试效率和测试质量。虽然在国外自动化测试工具应用已经很普遍,但国内许多企业对于软件自动化测试的理解还停留在表面上,没有深入的理解到企业…...
866363-70-4,N3-C5-NHS ester,叠氮-C5-NHS 主要物理性质分享
●外观以及性质:Azido-Aca-NHS淡黄色或无色油状,叠氮化物可以与炔烃、DBCO和BCN进行铜催化的点击化学反应。NHS酯可以与胺基反应,形成稳定的酰胺键。●中文名:叠氮-C5-NHS ester,6-叠氮己酸活性酯●英文名:…...
字符流定义及如何深入理解字符流的编码
IputSrem类和OupuSrem类在读写文件时操作的都是字节,如果希望在程序中操作字符,使用这两个类就不太方便,为此JDK提供了字符流。同字节流样,字符流也有两个抽象的顶级父类,分别是Reader和Writer其中,Reader是…...
什么是pod类型
很久很久以前,C 语言统一了江湖。几乎所有的系统底层都是用 C 写的,当时定义的基本数据类型有 int、char、float 等整数类型、浮点类型、枚举、void、指针、数组、结构等等。然后只要碰到一串01010110010 之类的数据,编译器都可以正确的把它解…...
大数据学习栈记——Neo4j的安装与使用
本文介绍图数据库Neofj的安装与使用,操作系统:Ubuntu24.04,Neofj版本:2025.04.0。 Apt安装 Neofj可以进行官网安装:Neo4j Deployment Center - Graph Database & Analytics 我这里安装是添加软件源的方法 最新版…...
linux之kylin系统nginx的安装
一、nginx的作用 1.可做高性能的web服务器 直接处理静态资源(HTML/CSS/图片等),响应速度远超传统服务器类似apache支持高并发连接 2.反向代理服务器 隐藏后端服务器IP地址,提高安全性 3.负载均衡服务器 支持多种策略分发流量…...
Appium+python自动化(十六)- ADB命令
简介 Android 调试桥(adb)是多种用途的工具,该工具可以帮助你你管理设备或模拟器 的状态。 adb ( Android Debug Bridge)是一个通用命令行工具,其允许您与模拟器实例或连接的 Android 设备进行通信。它可为各种设备操作提供便利,如安装和调试…...
20个超级好用的 CSS 动画库
分享 20 个最佳 CSS 动画库。 它们中的大多数将生成纯 CSS 代码,而不需要任何外部库。 1.Animate.css 一个开箱即用型的跨浏览器动画库,可供你在项目中使用。 2.Magic Animations CSS3 一组简单的动画,可以包含在你的网页或应用项目中。 3.An…...
mac 安装homebrew (nvm 及git)
mac 安装nvm 及git 万恶之源 mac 安装这些东西离不开Xcode。及homebrew 一、先说安装git步骤 通用: 方法一:使用 Homebrew 安装 Git(推荐) 步骤如下:打开终端(Terminal.app) 1.安装 Homebrew…...
计算机基础知识解析:从应用到架构的全面拆解
目录 前言 1、 计算机的应用领域:无处不在的数字助手 2、 计算机的进化史:从算盘到量子计算 3、计算机的分类:不止 “台式机和笔记本” 4、计算机的组件:硬件与软件的协同 4.1 硬件:五大核心部件 4.2 软件&#…...
(一)单例模式
一、前言 单例模式属于六大创建型模式,即在软件设计过程中,主要关注创建对象的结果,并不关心创建对象的过程及细节。创建型设计模式将类对象的实例化过程进行抽象化接口设计,从而隐藏了类对象的实例是如何被创建的,封装了软件系统使用的具体对象类型。 六大创建型模式包括…...
怎么让Comfyui导出的图像不包含工作流信息,
为了数据安全,让Comfyui导出的图像不包含工作流信息,导出的图像就不会拖到comfyui中加载出来工作流。 ComfyUI的目录下node.py 直接移除 pnginfo(推荐) 在 save_images 方法中,删除或注释掉所有与 metadata …...
nnUNet V2修改网络——暴力替换网络为UNet++
更换前,要用nnUNet V2跑通所用数据集,证明nnUNet V2、数据集、运行环境等没有问题 阅读nnU-Net V2 的 U-Net结构,初步了解要修改的网络,知己知彼,修改起来才能游刃有余。 U-Net存在两个局限,一是网络的最佳深度因应用场景而异,这取决于任务的难度和可用于训练的标注数…...
Visual Studio Code 扩展
Visual Studio Code 扩展 change-case 大小写转换EmmyLua for VSCode 调试插件Bookmarks 书签 change-case 大小写转换 https://marketplace.visualstudio.com/items?itemNamewmaurer.change-case 选中单词后,命令 changeCase.commands 可预览转换效果 EmmyLua…...
