参考 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 之类的数据,编译器都可以正确的把它解…...
PHP和Node.js哪个更爽?
先说结论,rust完胜。 php:laravel,swoole,webman,最开始在苏宁的时候写了几年php,当时觉得php真的是世界上最好的语言,因为当初活在舒适圈里,不愿意跳出来,就好比当初活在…...

23-Oracle 23 ai 区块链表(Blockchain Table)
小伙伴有没有在金融强合规的领域中遇见,必须要保持数据不可变,管理员都无法修改和留痕的要求。比如医疗的电子病历中,影像检查检验结果不可篡改行的,药品追溯过程中数据只可插入无法删除的特性需求;登录日志、修改日志…...

从深圳崛起的“机器之眼”:赴港乐动机器人的万亿赛道赶考路
进入2025年以来,尽管围绕人形机器人、具身智能等机器人赛道的质疑声不断,但全球市场热度依然高涨,入局者持续增加。 以国内市场为例,天眼查专业版数据显示,截至5月底,我国现存在业、存续状态的机器人相关企…...
Java - Mysql数据类型对应
Mysql数据类型java数据类型备注整型INT/INTEGERint / java.lang.Integer–BIGINTlong/java.lang.Long–––浮点型FLOATfloat/java.lang.FloatDOUBLEdouble/java.lang.Double–DECIMAL/NUMERICjava.math.BigDecimal字符串型CHARjava.lang.String固定长度字符串VARCHARjava.lang…...
【ROS】Nav2源码之nav2_behavior_tree-行为树节点列表
1、行为树节点分类 在 Nav2(Navigation2)的行为树框架中,行为树节点插件按照功能分为 Action(动作节点)、Condition(条件节点)、Control(控制节点) 和 Decorator(装饰节点) 四类。 1.1 动作节点 Action 执行具体的机器人操作或任务,直接与硬件、传感器或外部系统…...
Frozen-Flask :将 Flask 应用“冻结”为静态文件
Frozen-Flask 是一个用于将 Flask 应用“冻结”为静态文件的 Python 扩展。它的核心用途是:将一个 Flask Web 应用生成成纯静态 HTML 文件,从而可以部署到静态网站托管服务上,如 GitHub Pages、Netlify 或任何支持静态文件的网站服务器。 &am…...

RNN避坑指南:从数学推导到LSTM/GRU工业级部署实战流程
本文较长,建议点赞收藏,以免遗失。更多AI大模型应用开发学习视频及资料,尽在聚客AI学院。 本文全面剖析RNN核心原理,深入讲解梯度消失/爆炸问题,并通过LSTM/GRU结构实现解决方案,提供时间序列预测和文本生成…...

USB Over IP专用硬件的5个特点
USB over IP技术通过将USB协议数据封装在标准TCP/IP网络数据包中,从根本上改变了USB连接。这允许客户端通过局域网或广域网远程访问和控制物理连接到服务器的USB设备(如专用硬件设备),从而消除了直接物理连接的需要。USB over IP的…...

听写流程自动化实践,轻量级教育辅助
随着智能教育工具的发展,越来越多的传统学习方式正在被数字化、自动化所优化。听写作为语文、英语等学科中重要的基础训练形式,也迎来了更高效的解决方案。 这是一款轻量但功能强大的听写辅助工具。它是基于本地词库与可选在线语音引擎构建,…...

SiFli 52把Imagie图片,Font字体资源放在指定位置,编译成指定img.bin和font.bin的问题
分区配置 (ptab.json) img 属性介绍: img 属性指定分区存放的 image 名称,指定的 image 名称必须是当前工程生成的 binary 。 如果 binary 有多个文件,则以 proj_name:binary_name 格式指定文件名, proj_name 为工程 名&…...