技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTJS

Promise/A+ 教学实现:从 then 到 thenable 解析

所属馆藏
JavaScript
文件格式
Markdown
原始路径
JavaScript/94-完全符合Promise A+规范的Promise源码实现
本文目录10 个章节
  1. 前言
  2. 代码实现
  3. Promise Resolution Procedure
  4. then 的链式调用
  5. finally 的边界
  6. 静态组合方法的边界
  7. 如何运行 A+ 测试
  8. 生产环境建议
  9. 总结
  10. 参考资料

Promise/A+ 教学实现:从 then 到 thenable 解析

Category(分类): JavaScript Status: 已整理

原标题“完全符合 Promise/A+ 规范的 Promise 源码实现”保留作历史标题,但“通过 Promise/A+ 测试”只能证明特定 adapter 的 then 行为符合 A+ 测试,不能推出构造器、finally、静态组合器和所有 ECMAScript Promise 语义都完整兼容。本文把代码定位为教学实现,不替代生产环境的原生 Promise

前言

三年多以前,曾写过一篇 Promise 实现原理(附源码)。那篇文章展示了 Promise 的基本思想,但 Promise 规范边界、thenable 吸收、状态竞态和静态方法仍有很多细节。

Promises/A+ 的核心范围主要是 then 方法及 Promise Resolution Procedure,它并不规定 Promise 构造器如何暴露、如何调度任务、如何实现 finally 或静态组合器。ECMAScript 原生 Promise 在此基础上还定义了构造器、species、Promise jobs、all/race/allSettled/any 等语义。

本文的代码重点是:

  • pending、fulfilled、rejected 三种状态;
  • then 回调异步执行并按注册顺序触发;
  • handler 返回值决定下一个 Promise 的状态;
  • 读取 thenable 的 then 一次,并以第一次 resolve/reject 为准;
  • self-resolution、异常传播和常用静态方法的教学实现。

代码没有实现原生 Promise 的所有内部槽、species、品牌检查和宿主未处理 rejection 行为。生产代码请使用原生 Promise。

代码实现

'use strict'

const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

const isFunction = (value) => typeof value === 'function'
const isObjectOrFunction = (value) => (
  value !== null && (typeof value === 'object' || typeof value === 'function')
)

const enqueueJob = typeof queueMicrotask === 'function'
  ? queueMicrotask
  : (callback) => Promise.resolve().then(callback)

class MyPromise {
  constructor(executor) {
    if (!isFunction(executor)) {
      throw new TypeError('MyPromise resolver is not a function')
    }

    this._state = PENDING
    this._value = undefined
    this._handlers = []

    // alreadyResolved 与 _state 分开:resolve(pendingThenable) 后,
    // Promise 外观仍是 pending,但后续 reject 必须被忽略。
    let alreadyResolved = false

    const resolve = (value) => {
      if (alreadyResolved) return
      alreadyResolved = true
      this._resolveValue(value)
    }

    const reject = (reason) => {
      if (alreadyResolved) return
      alreadyResolved = true
      this._settle(REJECTED, reason)
    }

    try {
      executor(resolve, reject)
    } catch (error) {
      if (!alreadyResolved) {
        alreadyResolved = true
        this._settle(REJECTED, error)
      }
    }
  }

  _resolveValue(value) {
    if (value === this) {
      this._settle(REJECTED, new TypeError('Promise cannot resolve to itself'))
      return
    }

    if (!isObjectOrFunction(value)) {
      this._settle(FULFILLED, value)
      return
    }

    let then
    try {
      then = value.then
    } catch (error) {
      this._settle(REJECTED, error)
      return
    }

    if (!isFunction(then)) {
      this._settle(FULFILLED, value)
      return
    }

    // ECMAScript 会通过 PromiseResolveThenableJob 调用 thenable;
    // 这里使用 microtask 模拟“不会在 executor 中同步调用 thenable”。
    enqueueJob(() => {
      let called = false

      const resolveThenable = (nextValue) => {
        if (called) return
        called = true
        this._resolveValue(nextValue)
      }

      const rejectThenable = (reason) => {
        if (called) return
        called = true
        this._settle(REJECTED, reason)
      }

      try {
        then.call(value, resolveThenable, rejectThenable)
      } catch (error) {
        if (!called) {
          called = true
          this._settle(REJECTED, error)
        }
      }
    })
  }

  _settle(state, value) {
    if (this._state !== PENDING) return
    this._state = state
    this._value = value

    const handlers = this._handlers
    this._handlers = []
    for (const handler of handlers) {
      handler()
    }
  }

  then(onFulfilled, onRejected) {
    let resolveNext
    let rejectNext

    const nextPromise = new MyPromise((resolve, reject) => {
      resolveNext = resolve
      rejectNext = reject
    })

    const run = () => {
      enqueueJob(() => {
        const callback = this._state === FULFILLED
          ? onFulfilled
          : onRejected
        const value = this._value

        if (!isFunction(callback)) {
          if (this._state === FULFILLED) resolveNext(value)
          else rejectNext(value)
          return
        }

        try {
          resolveNext(callback(value))
        } catch (error) {
          rejectNext(error)
        }
      })
    }

    if (this._state === PENDING) {
      this._handlers.push(run)
    } else {
      run()
    }

    return nextPromise
  }

  catch(onRejected) {
    return this.then(undefined, onRejected)
  }

  finally(onFinally) {
    if (!isFunction(onFinally)) {
      return this.then(onFinally, onFinally)
    }

    return this.then(
      (value) => MyPromise.resolve(onFinally()).then(() => value),
      (reason) => MyPromise.resolve(onFinally()).then(() => {
        throw reason
      })
    )
  }

  static resolve(value) {
    if (value instanceof MyPromise) return value
    return new MyPromise((resolve) => resolve(value))
  }

  static reject(reason) {
    return new MyPromise((_, reject) => reject(reason))
  }

  static all(iterable) {
    return new MyPromise((resolve, reject) => {
      let values
      try {
        values = Array.from(iterable)
      } catch (error) {
        reject(error)
        return
      }

      if (values.length === 0) {
        resolve([])
        return
      }

      const result = []
      let remaining = values.length

      values.forEach((value, index) => {
        MyPromise.resolve(value).then(
          (item) => {
            result[index] = item
            remaining -= 1
            if (remaining === 0) resolve(result)
          },
          reject
        )
      })
    })
  }

  static race(iterable) {
    return new MyPromise((resolve, reject) => {
      let values
      try {
        values = Array.from(iterable)
      } catch (error) {
        reject(error)
        return
      }

      for (const value of values) {
        MyPromise.resolve(value).then(resolve, reject)
      }
      // 空 iterable 会保持 pending。
    })
  }

  static allSettled(iterable) {
    return new MyPromise((resolve, reject) => {
      let values
      try {
        values = Array.from(iterable)
      } catch (error) {
        reject(error)
        return
      }

      if (values.length === 0) {
        resolve([])
        return
      }

      const result = []
      let remaining = values.length

      values.forEach((value, index) => {
        MyPromise.resolve(value).then(
          (item) => {
            result[index] = { status: FULFILLED, value: item }
          },
          (reason) => {
            result[index] = { status: REJECTED, reason }
          }
        ).finally(() => {
          remaining -= 1
          if (remaining === 0) resolve(result)
        })
      })
    })
  }

  static any(iterable) {
    return new MyPromise((resolve, reject) => {
      let values
      try {
        values = Array.from(iterable)
      } catch (error) {
        reject(error)
        return
      }

      if (values.length === 0) {
        reject(new AggregateError([], 'All promises were rejected'))
        return
      }

      const errors = []
      let rejectedCount = 0

      values.forEach((value, index) => {
        MyPromise.resolve(value).then(
          resolve,
          (reason) => {
            errors[index] = reason
            rejectedCount += 1
            if (rejectedCount === values.length) {
              reject(new AggregateError(errors, 'All promises were rejected'))
            }
          }
        )
      })
    })
  }
}

// Promises/A+ 测试所需的 adapter 形式。
MyPromise.deferred = function deferred() {
  const result = {}
  result.promise = new MyPromise((resolve, reject) => {
    result.resolve = resolve
    result.reject = reject
  })
  return result
}

module.exports = MyPromise

上面的代码是一个可读的教学版本,但仍不是原生 Promise 的完全替代品。它使用 Array.from 简化了 iterator close 和构造器 species 语义,也没有完整实现子类、Symbol.species、品牌检查、未处理 rejection 追踪等能力。

Promise Resolution Procedure

then 的关键不是直接把返回值塞进下一个 Promise,而是运行解决过程:

  1. 如果返回值与下一个 Promise 是同一个对象,拒绝并抛出 self-resolution 错误;
  2. 如果返回值是对象或函数,读取一次它的 then 属性;
  3. then 不是函数时,把这个对象作为普通 fulfilled 值;
  4. then 是函数时,以该对象作为 this 调用它;
  5. thenable 多次调用 resolve/reject 时,只采用第一次;
  6. 读取 then 或调用 thenable 抛错时,按是否已经 settle 决定是否拒绝;
  7. resolve 传入的值仍可能是另一个 thenable,需要继续解析。
const thenable = {
  then(resolve) {
    resolve(42)
    resolve(100)
  }
}

MyPromise.resolve(thenable).then(console.log) // 42

构造器中的第一次 resolve 也会锁定结果,即使被解析的 thenable 还处于 pending:

let resolvePending
let rejectPending

const pending = new MyPromise((resolve, reject) => {
  resolvePending = resolve
  rejectPending = reject
})

const result = new MyPromise((resolve, reject) => {
  resolve(pending)
  rejectPending = reject
  throw new Error('late error')
})

// 第一次 resolve 已锁定 result;pending 尚未 settle,late error 不会覆盖它。
void result.catch(() => console.log('不会由 late error 触发'))

上面的例子重点是状态闩锁;真实代码不应把内部测试变量暴露为业务状态。

then 的链式调用

MyPromise.resolve(1)
  .then((value) => value + 1)
  .then((value) => {
    throw new Error(`value=${value}`)
  })
  .catch((error) => {
    console.log(error.message) // value=2
  })
  • then 总是返回一个新的 Promise;
  • handler 返回普通值时,下一个 Promise fulfilled;
  • handler 返回 Promise 或 thenable 时,下一个 Promise 等待它;
  • handler 抛异常时,下一个 Promise rejected;
  • 没有提供对应 handler 时,状态和值会透传;
  • Promise reaction 会异步执行,resolve()/reject() 函数本身不是“立即执行所有 then 回调”的微任务。

Promises/A+ 只要求 handler 在当前执行栈结束后异步执行,并不强制规定浏览器或 Node 必须使用某一个具体队列。现代 ECMAScript/浏览器实现通常通过 Promise job 和 microtask 接入。

finally 的边界

finally 的回调不接收 fulfilled 值或 rejection reason;它的返回值会被等待,回调抛错或返回 rejected Promise 会覆盖原结果;回调正常完成时,原来的值/原因继续传递。

MyPromise.resolve('value')
  .finally(() => console.log('cleanup'))
  .then(console.log) // cleanup,然后 value

MyPromise.reject(new Error('original'))
  .finally(() => MyPromise.resolve('cleanup'))
  .catch((error) => console.log(error.message)) // original

finally(undefined) 也应透传原结果,因此实现不能无条件执行 cb()。上面的教学实现修正了这个边界,但仍没有完整实现原生 Promise 的 species 机制。

静态组合方法的边界

  • MyPromise.all(iterable) 等待全部 fulfilled,结果按输入顺序排列,任意一项 rejected 就 rejected;
  • race(iterable) 采用第一个 settle 的结果,空 iterable 永久 pending;
  • allSettled(iterable) 等待全部结束,结果中包含 statusvaluereason
  • any(iterable) 采用第一个 fulfilled,全部 rejected 时使用 AggregateError;空 iterable 直接 rejected。

静态方法的原生语义还包括调用者构造器、C.resolve、iterator close、species 和异常处理。本文用 Array.from 和硬编码的 MyPromise.resolve 说明主线,不声称是完整 polyfill。

如何运行 A+ 测试

Promises/A+ 测试需要一个带 deferred() 的 adapter。代码中的 MyPromise.deferred 是对应形式:

const promisesAPlusTests = require('promises-aplus-tests')
const MyPromise = require('./my-promise')

promisesAPlusTests(MyPromise, (error) => {
  if (error) console.error(error)
  else console.log('A+ adapter tests completed')
})

“通过 872 个 case”只能说明测试套件覆盖的 then/resolution adapter 场景通过,不能证明 MyPromise.allfinally、静态方法、子类和宿主调度完全等同于原生 Promise。真正发布前还要补充 thenable、late reject、iterator、跨 realm 和不同宿主的测试。

生产环境建议

  • 直接使用浏览器和 Node.js 原生 Promise
  • 需要取消时使用 AbortController 等明确的取消协议,Promise 本身不会取消底层任务;
  • 通过 Error 实例保留 stack,不要随意 reject('failure')
  • 为并发任务选择 Promise.allallSettledany,并处理其失败契约;
  • 不要因为教学代码看起来短,就覆盖 Promise.prototype 或替换原生实现。

总结

Promise 的难点不在三态字段,而在 resolution procedure、thenable 吸收、一次性 settle、reaction job 和错误传播。手写实现适合学习,但“符合 A+”与“兼容 ECMAScript 原生 Promise”是两个不同的声明。

参考资料

作者:面条__ 原文链接:https://juejin.cn/post/7112639843871490061/ 来源:稀土掘金。

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS