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

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

在这里插入图片描述

前言

  这可能是手写promise较清晰的文章之一。

  由浅至深逐步分析了原生测试用例,以及相关Promise/A+规范。阅读上推荐以疑问章节为切入重点,对比Promise/A+规范与ECMAScript规范的内在区别与联系,确定怎样构建异步任务和创建promise实例。然后开始手写章节,过程中代码与测试可参考 promise-coding 仓库。

  也试着回答以下关键问题。

  • 什么是广义对象?
  • 如何检验promise类型?
  • promisethenable两类型有何区别?

疑问

  如果不太清楚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.resolvePromise.allPromise.race等,新增了原型方法Promise.prototype.catchPromise.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 补充可用宏任务setTimeoutsetImmediate,或者微任务MutationObserver(浏览器环境)和process.nextTicknode环境)达到异步。

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包括resolvereject两个函数参数,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}
}

方法传参

  原生Promisethen方法传参。

const p = new Promise((resolve, reject) => {resolve()
})p.then(undefined, undefined)

  相关特征包括。

  • promise必须有then方法,且接收两个参数onFulfilledonRejected——「Promise/A+ 2.2」
  • onFulfilledonRejected都是可选参数,若不是函数,必须被忽略——「Promise/A+ 2.2.1」
  • onFulfilledonRejected一定被作为普通函数调用——「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
})

  相关特征包括。

  • 如果onFulfilledonRejected是一个函数,必须在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方法内部没有等待态相关逻辑,也就没有任何操作。1sresolve执行,也仅仅是将p状态修改为解决态。

  如何解决呢?

  可以在等待态就保存onFulfilledonRejected函数,在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方法函数参数按语法顺序执行
  • 同一promisethen方法可能被多次调用——「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,
// }

  相关特征包括。

  • 如果onFulfilledonRejected返回一个值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,
// }

  那么如何在onFulfilledonRejected返回数值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执行器上,resolvereject两个参数也可修改状态。

  如果将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保存至数组中。1sresolve执行,p1状态修改为解决态,紧接着取出运行onFulfilledp2状态无任何变化。

  我们可以在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函数,参数暂时为xresolve

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.

  可确认参数promisexresolvereject

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
})

  相关特征包括。

  • 如果onFulfilledonRejected抛出一个异常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保存。1sresolve执行,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

  相关特征包括。

  • onFulfilledonRejected必须以异步形式运行——「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>,
// }

  相关特征包括。

  • 如果promisex引用同一对象,则拒绝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

  那我们就可在onFulfilledonRejected内部去修改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是一个对象或函数(广义对象)
    • thenx.then
    • 如果获取x.then导致抛出了一个异常e,用e作为原因拒绝promise
    • 如果then是一个函数,用x作为this调用它,且包含两个参数,分别为resolvePromiserejectPromise
      • 如果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是一个函数...
    • ...
    • ...
    • 如果resolvePromiserejectPromise都被调用,或者对其中一个多次调用,那么第一次调用优先,以后的调用都会被忽略
    • 如果调用then抛出了...
      • 如果resolvePromiserejectPromise已经被调用,则忽略它
      • ...

  为了限制哪种情况呢?

  还是来看一个原生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,标记是否运行参数resolvePromiserejectPromise。然后在第一次运行时将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 {...}
}

  还有方法可确定xpromise类型吗?答案是没有。

  怎么办呢?

  既然无法检验promise类型,那就退而求其次,检验类似promise类型的,即鸭式辩型。

鸭子类型(duck typing),也叫鸭式辩型,一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子

  规范「Promise/A+ 1.2」提出了thenable类型,即定义了then方法的对象或函数。

{then() {...},
}

thenablepromise的鸭子类型

  检验是否为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较清晰的文章之一。 由浅至深逐步分析了原生测试用例&#xff0c;以及相关Promise/A规范。阅读上推荐以疑问章节为切入重点&#xff0c;对比Promise/A规范与ECMAScript规范的内在区别与联系&#xff0c;确定怎样构建异步任务和创建promise实例。然后开…...

yolov5数据集制作

yolov5 数据集的格式 每个图像的标注信息存储在一个独立的txt文件中每个txt文件的名称应该与其对应的图像名称相同,只是文件扩展名不同。例如: 对于名为“image1.jpg”的图像,其标注信息应存储在名为“image1.txt”的txt文件中。 在每个txt文件中,每一行表示一个对象的标注…...

主板EC程序烧写异常致无法点亮修复经验

主板型号&#xff1a;Gigabyte AB350M-Gaming3 官网上明确写着支持R5 5500&#xff0c;但按照如下步骤实践下来实际是不支持的 升级biosF31到F40版本的注意事项&#xff1a; 步骤&#xff1a; 1 使用Q-Flash先将bios升级到f31版本&#xff1b;2 然后下载提示中的ECFW Update To…...

【Java爬取赛事网站】命令行输出(仅供学习)

Java爬取赛事网站 Java爬取赛事网站Java爬取赛事网站参与社区的问题回答Gitcode项目地址PSP表格解题思路描述问题接口设计和实现过程编写中的测试关键代码展示性能改进单元测试异常处理心路历程与收获参与社区的问题回答 问题回答这个作业属于哪个课程软件工程-23年春季学期这…...

redis主从复制原理

在 Redis 中&#xff0c;我们可以通过 SLAVEOF 命令或者 slaveof 选项&#xff0c;让一个服务器去复制另一个服务器&#xff0c;被复制的服务器称为“主服务器”&#xff0c;发起复制的服务器称为“从服务器”&#xff0c;由两种服务器组成的模式称为“主从复制”。 主从复制原…...

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. 每日温度 - 力扣&#xff08;LeetCode&#xff09; 给定一个整数数组 temperatures &#xff0c;表示每天的温度&#xff0c;返回一个数组 answer &#xff0c;其中 answer[i] 是指对于第 i 天&#xff0c…...

ChIP-seq 分析:Peak 注释与可视化(9)

1. 基因注释 到目前为止&#xff0c;我们一直在处理对应于转录因子结合的 ChIPseq 峰。顾名思义&#xff0c;转录因子可以影响其靶基因的表达。 转录因子的目标很难单独从 ChIPseq 数据中确定&#xff0c;因此我们通常会通过一组简单的规则来注释基因的峰&#xff1a; 如果峰与…...

ABB机器人配置DeviceNet总线IO板以及信号分配的具体方法示例

ABB机器人配置DeviceNet总线IO板以及信号分配的具体方法示例 基本步骤: 配置IO板分配IO信号这里以DeviceNet总线的DSQC652为例进行说明: 配置IO板的基本步骤:  配置IO板的型号  连接到总线  配置IO板的地址 (1台机器人可以配置多个IO板连接到DeviceNet总线,为了让机…...

2023 年网络安全漏洞的主要原因

​  网络安全漏洞已经并将继续成为企业面临的主要问题。因此&#xff0c;对于企业领导者来说&#xff0c;了解这些违规行为的原因至关重要&#xff0c;这样他们才能更好地保护他们的数据。 在这篇博文中&#xff0c;我们将概述 2023 年比较普遍的网络安全漏洞的主要原因。 云…...

剑指 Offer 34. 二叉树中和为某一值的路径

剑指 Offer 34. 二叉树中和为某一值的路径 难度&#xff1a;middle\color{orange}{middle}middle 题目描述 给你二叉树的根节点 rootrootroot 和一个整数目标和 targetSumtargetSumtargetSum &#xff0c;找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。 叶子节…...

2023前端vue面试题(边面边更)

Vue中key的作用 vue 中 key 值的作用可以分为两种情况来考虑&#xff1a; 第一种情况是 v-if 中使用 key。由于 Vue 会尽可能高效地渲染元素&#xff0c;通常会复用已有元素而不是从头开始渲染。因此当使用 v-if 来实现元素切换的时候&#xff0c;如果切换前后含有相同类型的…...

webpack配置完全指南

前言 对于入门选手来讲&#xff0c;webpack 配置项很多很重&#xff0c;如何快速配置一个可用于线上环境的 webpack 就是一件值得思考的事情。其实熟悉 webpack 之后会发现很简单&#xff0c;基础的配置可以分为以下几个方面&#xff1a; entry 、 output 、 mode 、 resolve …...

juju创建lxd容器时如何使用本地镜像(by quqi99)

作者&#xff1a;张华 发表于&#xff1a;2023-03-01 版权声明&#xff1a;可以任意转载&#xff0c;转载时请务必以超链接形式标明文章原始出处和作者信息及本版权声明 问题 没有外网&#xff0c;所以配置了一个local custom镜像库&#xff0c;也使用了container-image-meta…...

后端程序员学习前端开发之第一步环境搭建

一、安装 Node.js Node.js 是一个开源的、跨平台的 JavaScript 运行时环境。Node.js官网 二、安装 npm 镜像 因为 npm 是国外的&#xff0c;所以使用起来速度比较慢。我们这里使用了淘宝的 cnpm 镜像安装 vue。使用淘宝的 cnpm 命令管理工具代替默认的 npm 管理工具。 进入c…...

【记录问题】RuntimeError:working outside of application context. Flask使用SQLAlchemy数据库

前提&#xff1a;Flask使用SQLAlchemy数据库 本质&#xff1a;依赖包版本不匹配 问题1&#xff1a;报错RuntimeError&#xff1a;working outside of application context. 运行程序报错&#xff0c;如下错误&#xff1a; 原因&#xff1a;flask-sqlalchemy 版本过高导致&am…...

自动化测试难点案例分析,其实自动化你用错方向还不如不用

随着国内企业软件开发及测试水平的提升&#xff0c;许多企业开始尝试开展自动化测试的应用&#xff0c;以提高测试效率和测试质量。虽然在国外自动化测试工具应用已经很普遍&#xff0c;但国内许多企业对于软件自动化测试的理解还停留在表面上&#xff0c;没有深入的理解到企业…...

866363-70-4,N3-C5-NHS ester,叠氮-C5-NHS 主要物理性质分享

●外观以及性质&#xff1a;Azido-Aca-NHS淡黄色或无色油状&#xff0c;叠氮化物可以与炔烃、DBCO和BCN进行铜催化的点击化学反应。NHS酯可以与胺基反应&#xff0c;形成稳定的酰胺键。●中文名&#xff1a;叠氮-C5-NHS ester&#xff0c;6-叠氮己酸活性酯●英文名&#xff1a;…...

字符流定义及如何深入理解字符流的编码

IputSrem类和OupuSrem类在读写文件时操作的都是字节&#xff0c;如果希望在程序中操作字符&#xff0c;使用这两个类就不太方便&#xff0c;为此JDK提供了字符流。同字节流样&#xff0c;字符流也有两个抽象的顶级父类&#xff0c;分别是Reader和Writer其中&#xff0c;Reader是…...

什么是pod类型

很久很久以前&#xff0c;C 语言统一了江湖。几乎所有的系统底层都是用 C 写的&#xff0c;当时定义的基本数据类型有 int、char、float 等整数类型、浮点类型、枚举、void、指针、数组、结构等等。然后只要碰到一串01010110010 之类的数据&#xff0c;编译器都可以正确的把它解…...

2023年中小企业实施智能制造的建议

智能制造的载体是制造系统&#xff0c;制造系统从微观到宏观有不同的层次&#xff0c;主要包括制造装备、制造单元、制造车间&#xff08;工厂&#xff09;、制造企业和企业生态等。随着智能制造的深入推进&#xff0c;未来智能制造将向以下五个方向发展。 &#xff08;一&…...

【LeetCode】剑指 Offer 19. 正则表达式匹配 p124 -- Java Version

题目链接&#xff1a;https://leetcode.cn/problems/zheng-ze-biao-da-shi-pi-pei-lcof/ 1. 题目介绍&#xff08;19. 正则表达式匹配&#xff09; 请实现一个函数用来匹配包含. 和*的正则表达式。模式中的字符.表示任意一个字符&#xff0c;而’*表示它前面的字符可以出现任意…...

linux和windows中安装emqx消息服务器

大家好&#xff0c;我是雄雄&#xff0c;欢迎关注微信公众号雄雄的小课堂 现在是&#xff1a;2023年3月1日21:53:55 前言 最近几天看了下mqtt&#xff0c;通过不断的搜索资料&#xff0c;也将mqtt集成到项目中&#xff0c;跑了个demo运行&#xff0c;和预想中的差不多&#x…...

【XXL-JOB】XXL-JOB的搭建和使用

【XXL-JOB】XXL-JOB的搭建和使用 文章目录【XXL-JOB】XXL-JOB的搭建和使用1. 任务调度1.1 实现任务调度1.1.1 多线程实现1.1.2 Timer实现1.1.3 ScheduledExecutor实现2. 分布式任务调度2.1 采用分布式的原因3. XXL-JOB3.1 XXL-JOB介绍3.2 执行流程4. 搭建XXL-JOB4.1 创建数据库…...

HCIP-5OSPF基本原理及基本配置学习笔记

1、OSPF基本原理 开放式最短路径优先OSPF&#xff08;Open Shortest Path First&#xff09;协议是IETF定义的一种基于链路状态的内部网关路由协议。 RIP是一种基于距离矢量算法的路由协议&#xff0c;存在着收敛慢、易产生路由环路、可扩展性差等问题&#xff0c;目前已逐渐被…...

Migrate your data into databend with DataX

现在互联网应用越来越复杂&#xff0c;每个公司都会有多种多样的数据库。通常是用最好的硬件来跑 OLTP&#xff0c;甚至还在 OLTP 中进行分库分表来满足业务&#xff0c;这样对于一些分析&#xff0c;聚合&#xff0c;排序操作非常麻烦。这也有了异构数据库的数据同步需求&…...

ssh: Permission denied (publickey,gssapi-keyex,gssapi-with-mic,password)

【ansible 设置host为localhost&#xff0c;执行ping命令报错】 [eniq-slocalhost ansible]$ ansible all -m ping -i inventory localhost | UNREACHABLE! > { "changed": false, "msg": "Failed to connect to the host via ssh: Perm…...

有限元中三角形的一些积分公式

文章目录有限元中三角形的相关积分公式有限元中三角形的相关积分公式 在 xyxyxy 平面中&#xff0c; 通过三个点 (xi,yi),(xj,yj),(xm,ym)(x_i, y_i), (x_j, y_j), (x_m, y_m)(xi​,yi​),(xj​,yj​),(xm​,ym​) 定义一个三角形&#xff0c; 令坐标原点位于其中心(或者重心)…...

【docker-compose】安装mongodb

1. 安装方式 压缩包容器安装docker&#xff08;推荐&#xff0c;一分钟安装&#xff09; 2. 环境 linux服务器已安装好 docker docker-compose &#xff08;不了解的客官&#xff0c;请点击进入&#xff09; 3. 步骤&#xff1a; Step 1&#xff1a; linux下建立如下目录…...

【ClickHouse源码】物化视图的写入过程

本文对 ClickHouse 物化视图的写入流程源码做个详细说明&#xff0c;基于 v22.8.14.53-lts 版本。 StorageMaterializedView 首先来看物化视图的构造函数&#xff1a; StorageMaterializedView::StorageMaterializedView(const StorageID & table_id_,ContextPtr local_…...

在微信中做网站/做百度推广多少钱

1.概念&#xff1a; 一个完整的JavaScript实现应该由以下三个部分构成&#xff1a;ECMAScript、DOM、BOM. ECMAScript: ES规定了JS的变成语法和基础核心知识&#xff0c;是所有浏览器厂商都遵守的JS语法工业标准。 DOM: 文档对象模型&#xff08;Document Object Model&#xf…...

做爰的最好看的视频的网站/女排联赛排名

1.选择控制面板----> 选择 程序 ————> 选择 打开或关闭windows功能2.此时回弹出一个对话框&#xff0c;将telnet客户端打上√注意&#xff1a;不要讲telnet服务端打上√转载于:https://blog.51cto.com/linux2585/1540136...

内蒙古建设工程交易中心网站/手机百度浏览器

TimeLimitingCollector 包装其他的收集器&#xff0c;当查询超过指定时间时通过抛出TimeExceededException异常来中止搜索。通过一个被包装的收集器&#xff0c;一个时钟定时器和超时时间来构造TimeLimitingCollector对象。setBaseline(long clockTime)&#xff1a;在包…...

网站开发后端 书/排超最新积分榜

Day1 题目描述 计算字符串最后一个单词的长度&#xff0c;单词以空格隔开。 输入描述: 一行字符串&#xff0c;非空&#xff0c;长度小于5000。 输出描述: 整数N&#xff0c;最后一个单词的长度。 示例1 输入 hello world 输出 5 #include <iostream> #inclu…...

景区网站建设的好处/seo营销推广全程实例

2019独角兽企业重金招聘Python工程师标准>>> 解决方法&#xff1a; project->clean 参考链接&#xff1a;https://jingyan.baidu.com/article/cbcede07107d9802f40b4dff.html 转载于:https://my.oschina.net/qimhkaiyuan/blog/2991369...

郑州营销型网站制作运营/企拓客app骗局

目录HBASE架构HBASEshellHBASE的JavaAPI操作HBASE连接HiveHBASE过滤器比较器比较运算符常见的比较器过滤器常见的过滤器过滤器举例过滤器举例的所有代码HBASE架构 HBASEshell 不做概述 HBASE的JavaAPI操作 hbase的api操作总结下来就是一句话&#xff0c;就是 需要对表做一些…...