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

news2024/9/29 9:22:37

在这里插入图片描述

前言

  这可能是手写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 Error

  deferred.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) // 2

new 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] = FULFILLED
    this[PromiseResult] = value
  }
  #reject = reason => {
    this[PromiseState] = REJECTED
    this[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] = FULFILLED
    this[PromiseResult] = value
  }
}
#reject = reason => {
  if (this[PromiseState] === PENDING) {
    this[PromiseState] = REJECTED
    this[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 = onFulfilled
      this.#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 = queueMicrotask

class 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) // 1

    resolve(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 => value
  onRejected = 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 value

  return 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 then

      try {
        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 = false

  try {
    then.call(
      x,
      y => {
        if (called) return
        called = true

        resolvePromise(promise, y, resolve, reject)
      },
      r => {
        if (called) return
        called = true

        reject(r)
      }
    )
  } catch (e) {
    if (called) return
    called = true

    reject(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 then

    try {
      then = x.then
    } catch (e) {
      reject(e)
    }

    if (typeof then === 'function') {
      var called = false

      try {
        then.call(
          x,
          y => {
            if (called) return
            called = true

            resolvePromise(promise, y, resolve, reject)
          },
          r => {
            if (called) return
            called = true

            reject(r)
          }
        )
      } catch (e) {
        if (called) return
        called = true

        reject(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 = resolve
    result.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 同步更新,欢迎关注😉~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/381374.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

JavaSE18-面向对象-内部类

文章目录一、局部内部类二、成员内部类三、静态内部类四、匿名内部类一、局部内部类 把类定义在方法中。对象创建格式&#xff1a;直接在定义内部类的方法中创建。如果在该方法外就不能使用该局部内部类了。应用场景&#xff1a;如果需要定义一个在方法中临时使用的类可以使用…

SpringCloud(微服务)学习篇(一)

SpringCloud(微服务)学习篇(一) 1 nacos的下载和配置 1.1 进入官网 nacos官网 1.2 点击nacos➡点击最新稳定版本 1.3 往下翻并点击nacos-server-2.2.0.zip,此时就已经开始下载了 1.4 把下载好的压缩包解压到没有中文路径的目录里面 1.5 修改application.properties文件 1.…

Testlink相关功能使用部分总结

1.首页面&#xff08;普通用户&#xff0c;测试用例创建用户的权限&#xff09; 右上角切换具体的项目&#xff1b;页面上方包含主页、用例、测试执行、测试结果&#xff1b;左侧包含测试项目管理、关键字管理、编辑测试用例、搜索测试用例、每用户创建的测试用例&#xff1b;…

Element UI的基本使用

学习来源&#xff0c;传送门 目录创建vue项目Element UI主要的标签Vue router 来动态创建左侧导航栏为何会发生嵌套menu与router的绑定设置默认展开设置默认打开页面创建vue项目 以管理员身份&#xff0c;在选定目录下&#xff0c;使用vue ui 按照正常配置配好&#xff0c;可…

Linux(ubuntu)系统搭建docker下的LNMP环境

系统环境 系统&#xff1a;Ubuntu 18.04.4 LTS x86_64 管理面板&#xff1a;宝塔面板7.9.8 下载镜像 通过面板下载docker和docker-compose 下载完毕后通过docker->镜像->从仓库拉取拉取镜像ubuntu:20.04 或者通过docker pull ubuntu:20.04拉取镜像 通过docker->容…

代码随想录算法训练营第四十一天 | 01背包问题-二维数组滚动数组,416. 分割等和子集

一、参考资料01背包问题 二维 https://programmercarl.com/%E8%83%8C%E5%8C%85%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%8001%E8%83%8C%E5%8C%85-1.html 视频讲解&#xff1a;https://www.bilibili.com/video/BV1cg411g7Y6 01背包问题 一维 https://programmercarl.com/%E8%83%8C%E5…

大功率分流电阻器产品阵容进一步扩大,助力大功率应用小型化

全球知名半导体制造商ROHM&#xff08;总部位于日本京都市&#xff09;面向车载、工业设备及白色家电等大功率应用&#xff0c;开发出大功率低阻值分流电阻器“GMR 系列”中额定功率最大的、10W 电阻器“GMR320”。近年来&#xff0c;在车载领域和工业设备领域中&#xff0c;应…

什么是敏捷测试

敏捷 反应快速灵敏。 在敏捷软件开发领域&#xff0c;更注重的以人为核心&#xff0c;迭代&#xff0c;循序渐进的开发方法。相比传统的开发方法&#xff0c;这种方法能更快速的开发&#xff0c;上线&#xff0c;反馈&#xff0c;调整、迭代。以敏捷的姿态去发展产品。 敏捷与…

基于java+swing+mysql员工工资管理系统

基于javaswingmysql员工工资管理系统一、系统介绍二、功能展示1.用户登陆2.员工主要功能3.管理员主要功能三、系统实现1.StudentFrame .java四、其它1.其他系统实现2.获取源码一、系统介绍 该项目功能相对完善&#xff0c;有管理员和普通用户两个角色&#xff0c;分别实现了一…

磷脂酰丝氨酸的作用;CAS:383907-32-2;磷脂酰丝氨酸(phosphatidylserine,PS)

磷脂酰丝氨酸&#xff08;phosphatidylserine&#xff0c;PS&#xff09;又称丝氨酸磷脂&#xff0c;二酰甘油酰磷酸丝氨酸&#xff0c;简称PS&#xff0c;是一类普遍存在的磷脂&#xff0c;通常位于细胞膜的内层&#xff0c;磷酯化合物中的磷酸甘油酯类&#xff0c;是细胞膜组…

数字化时代,企业如何通过技术改造传统客户服务模式

B端产品要从流量思维到客户思维上转变&#xff0c;良好的客户服务能够让B端企业走的越远走的越多&#xff0c;当然&#xff0c;对于所有产品或者企业来说&#xff0c;也唯有客户服务才是走的更远更久的保证&#xff0c;优秀的客户服务团队是企业的潜在优质财富。 搭建客服团队…

ATTO 647N-NHS ester,ATTO 647N SE,ATTO 647N NHS酯,适用于单分子检测应用

基础产品数据&#xff08;Basic Product Data&#xff09;&#xff1a;CAS号&#xff1a;N/A中文名&#xff1a;ATTO 647N-琥珀酰亚胺酯&#xff0c;ATTO 647N-活性酯&#xff0c;ATTO 647N NHS酯英文名&#xff1a;ATTO 647 N-NHS&#xff0c;ATTO647N-NHS&#xff0c;ATTO 64…

为什么转行IT行业都会选择学习Python?

现在学习Python的人越来越多了&#xff0c;很多人都疑惑为什么这么多人转行IT都会选择学Python?为什么学Python要参加Python培训班呢?接下来蛋糕为大家答疑解惑一下吧。 虽然Python已经非常普及&#xff0c;但是大部分的大学都还没有开设Python课程&#xff0c;如果想要学习…

XGBoost学习-应用案例

文章目录一、过拟合&#xff1a;剪枝参数与回归模型调参二、XGBoost模型的保存和调用使用Joblib保存和调用模型三、分类案例&#xff1a;XGB中的样本不均衡问题四、 XGBoost类中的其他参数和功能总结一、过拟合&#xff1a;剪枝参数与回归模型调参 class xgboost.XGBRegressor…

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

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

只用最适合的 | 主流 .NET 报表控件全面对比

随着 .NET 平台的出现&#xff0c;报表相关的开发控件随着而来&#xff0c;已经有若干成熟的产品可供开发人员使用&#xff0c;本文旨在通过从不同维度对比目前最流行的3款 .NET报表控件&#xff1a;FastReport、Stimulsoft、水晶报表&#xff0c;给所有报表开发人员在做产品选…

家用洗地机哪款最好用?全球洗地机十大品牌

近年来&#xff0c;智能家用电器洗地机已经融入到我们生活中了&#xff0c;成为最受欢迎的清洁工具了&#xff0c;家用洗地机吸拖洗一体&#xff0c;不用先扫后拖那么麻烦&#xff0c;只需轻轻一推&#xff0c;就能把扫地、拖地、擦地的活全干了&#xff0c;操作简单&#xff0…

【iOS】—— 初识RAC响应式编程

RAC&#xff08;ReactiveCocoa&#xff09; 文章目录RAC&#xff08;ReactiveCocoa&#xff09;响应式编程和函数式编程的区别函数式编程响应式编程响应式编程的优点RAC操作1.利用button点击实现点击事件和传值2.RACSignal用法RACSignal总结&#xff1a;3.对于label的TapGestur…

Java | IO 模式之 JavaNIO 应用

文章目录NIO1 Java NIO 基本介绍2 NIO 和 BIO 的比较3 NIO 三大核心原理示意图3.1 Buffer缓冲区3.2 Channel&#xff08;通道&#xff09;3.3 Selector选择器3.4 总结4 NIO核心一&#xff1a;缓冲区(Buffer)4.1 缓冲区&#xff08;Buffer&#xff09;4.2 Buffer 类及其子类4.3 …

泛型与Map接口

Java学习之道 泛型 泛型这种参数类型可以用在类、方法和接口中&#xff0c;分别被称为泛型类&#xff0c;泛型方法&#xff0c;泛型接口 参数化类型&#xff1a;将类型由原来的具体的类型参数化&#xff0c;在使用/调用时传入具体的类型JDK5引入特性提供了安全检测机制&#xf…