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

显示模式

登录
ARCHIVE DOCUMENTJS

用 TinyPromise 演示异步链式调用(约 20 行的动机)

所属馆藏
JavaScript
文件格式
Markdown
原始路径
JavaScript/104-最简实现Promise,支持异步链式调用(20行)
本文目录7 个章节
  1. 前言
  2. 成功路径教学动机
  3. TinyPromise 教学版
  4. 调度说明
  5. 总结
  6. 原文归属
  7. 参考链接

用 TinyPromise 演示异步链式调用(约 20 行的动机)

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

前言

面试时经常有人要求实现一个 Promise。如果按 ECMAScript 和 Promises/A+ 的全部边界来做,可能讲到天黑也结束不了。原文用约 20 行代码展示“成功值如何沿链传递”的动机仍然很好,但那段代码不能叫 Promise polyfill,也不符合 Promises/A+:它缺少拒绝、状态锁、晚注册 handler、异常传播、缺省 handler、thenable resolution 等核心语义。

本文把实现命名为 TinyPromise,不覆盖全局 Promise,用一个可运行的教学版补上这些关键机制。它仍然不是生产 polyfill,也没有实现原生 Promise 的所有静态方法、迭代器边界、调试钩子和全部规范细节。生产代码请使用宿主原生 Promise

成功路径教学动机

先保留原来的链式场景:第一个异步操作完成后输出 1,第二个异步操作完成后输出 2

new TinyPromise((resolve) => {
  setTimeout(() => {
    resolve(1)
  }, 500)
})
  .then((value) => {
    console.log(value)

    return new TinyPromise((resolve) => {
      setTimeout(() => {
        resolve(2)
      }, 500)
    })
  })
  .then(console.log)

典型表现是约 500ms 后输出 1,再约 500ms 后输出 2。重点不在于行数,而在于:then() 返回一个新的 Promise;handler 返回的值会决定这个新 Promise 的结果;如果 handler 返回 thenable,新 Promise 会采用它的状态。

TinyPromise 教学版

const enqueue = typeof queueMicrotask === 'function'
  ? queueMicrotask
  : (fn) => setTimeout(fn, 0)

class TinyPromise {
  constructor(executor) {
    if (typeof executor !== 'function') {
      throw new TypeError('executor 必须是函数')
    }

    this.state = 'pending'
    this.value = undefined
    this.handlers = []

    let locked = false

    const fulfill = (value) => {
      if (this.state !== 'pending') return
      this.state = 'fulfilled'
      this.value = value
      this.flush()
    }

    const rejectState = (reason) => {
      if (this.state !== 'pending') return
      this.state = 'rejected'
      this.value = reason
      this.flush()
    }

    // 这是教学版的 resolution procedure 核心:不使用 instanceof 判断。
    // seen 是额外的教学防护;它不等同于 ECMAScript 对所有间接 thenable
    // 环的处理,生产代码不应据此声称与原生 Promise 完全一致。
    const resolveValue = (value, seen = new Set()) => {
      if (value === this) {
        rejectState(new TypeError('Promise 不能解析为自身'))
        return
      }

      if (
        value !== null &&
        (typeof value === 'object' || typeof value === 'function')
      ) {
        if (seen.has(value)) {
          rejectState(new TypeError('检测到 thenable 循环'))
          return
        }

        let then
        try {
          then = value.then
        } catch (error) {
          rejectState(error)
          return
        }

        if (typeof then === 'function') {
          seen.add(value)
          enqueue(() => {
            let called = false

            const resolveThenable = (nextValue) => {
              if (called) return
              called = true
              resolveValue(nextValue, seen)
            }

            const rejectThenable = (reason) => {
              if (called) return
              called = true
              rejectState(reason)
            }

            try {
              then.call(value, resolveThenable, rejectThenable)
            } catch (error) {
              if (!called) rejectThenable(error)
            }
          })
          return
        }
      }

      fulfill(value)
    }

    const resolve = (value) => {
      // resolved 与 settled 不完全相同:解析 thenable 时已锁定,但仍可能 pending。
      if (locked) return
      locked = true
      resolveValue(value)
    }

    const reject = (reason) => {
      if (locked) return
      locked = true
      rejectState(reason)
    }

    try {
      // executor 是同步调用的;它的异常会变成 rejection。
      executor(resolve, reject)
    } catch (error) {
      reject(error)
    }
  }

  flush() {
    const handlers = this.handlers.splice(0)
    for (const handler of handlers) {
      enqueue(handler)
    }
  }

  then(onFulfilled, onRejected) {
    return new TinyPromise((resolve, reject) => {
      const run = () => {
        const handler = this.state === 'fulfilled'
          ? onFulfilled
          : onRejected

        if (typeof handler !== 'function') {
          const pass = this.state === 'fulfilled' ? resolve : reject
          pass(this.value)
          return
        }

        try {
          // resolve 会处理普通值、thenable 和 self-resolution。
          resolve(handler(this.value))
        } catch (error) {
          reject(error)
        }
      }

      if (this.state === 'pending') {
        this.handlers.push(run)
      } else {
        enqueue(run)
      }
    })
  }

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

代码中保留的核心状态

  • pending:尚未完成,handler 暂存于 handlers
  • fulfilled:成功完成,value 保存兑现值。
  • rejected:失败完成,value 保存拒绝原因。
  • locked:第一次调用公开的 resolvereject 后锁定解析权。resolve(thenable) 时,TinyPromise 可能仍是 pending,直到 thenable 最终完成。
  • flush():settled 后取出已有 handler 并异步调度;settled 之后再调用 then() 也会异步执行,不会永远丢失。

解析返回值,而不是判断实例类型

then() 的 handler 可能返回普通值、原生 Promise、另一个 TinyPromise 或任意 thenable。下面的 thenable 没有继承任何 Promise 类,但仍会被吸收:

new TinyPromise((resolve) => {
  resolve({
    then(resolveThenable) {
      resolveThenable(42)
    },
  })
}).then((value) => {
  console.log(value)
})

因此不能用 value instanceof Promise 代替 resolution procedure。instanceof 无法覆盖跨 realm Promise、其他实现和普通 thenable;规范语义是读取一次 then,调用它,并用一次性守卫处理 resolve/reject。

缺省 handler、异常和拒绝传播

new TinyPromise((resolve) => {
  resolve(1)
})
  .then((value) => value + 1)
  .then(() => {
    throw new Error('handler failed')
  })
  .catch((error) => {
    console.log(error.message)
  })

new TinyPromise((resolve, reject) => {
  reject(new Error('network failed'))
})
  .then(undefined, (error) => {
    console.log(error.message)
    return 'recovered'
  })
  .then(console.log)

缺省的 fulfillment handler 会透传值,缺省的 rejection handler 会透传原因;handler 抛出的异常会使下一个 TinyPromise rejected。第一次 resolvereject 胜出,后续调用不会再次改变状态或重复执行 handler。

晚注册和 self-resolution

const late = new TinyPromise((resolve) => {
  resolve('late value')
})

setTimeout(() => {
  late.then(console.log)
}, 0)

let chained
chained = new TinyPromise((resolve) => {
  resolve(1)
}).then(() => chained)

chained.catch((error) => {
  console.log(error.message)
})

第一个例子验证 settled 后注册的 handler 仍会被调度;第二个例子验证链不能解析为自身。教学版还用 Set 防止简单 thenable 环;这是额外的教学防护,不是原生 Promise 对所有间接 thenable 环的等价保证。复杂恶意 thenable、调度细节和所有规范边界仍应交给成熟实现。

调度说明

代码优先使用 queueMicrotask(),没有该 API 时退回 setTimeout(fn, 0)。退回方案只保证“不是当前同步调用栈立即执行”,不等同于原生 Promise reaction 的 microtask:timer 是宿主 task,可能与浏览器渲染、Node 的 nextTick/V8 microtask 和其他 I/O 有不同顺序。

Promises/A+ 要求 handler 异步执行并规定 then 与 Promise Resolution Procedure 的核心契约,但它不覆盖 ECMAScript Promise 构造器的所有静态 API,也不单独规定浏览器/Node 的调度队列。要声称实现符合 A+,应补齐边界并运行官方测试,而不是只看成功路径能打印 1、2

总结

这段 TinyPromise 适合课堂演示以下概念:

  1. executor 同步执行,handler 异步调度。
  2. pendingfulfilledrejected 三态不可逆。
  3. 第一次 resolve/reject 锁定解析权。
  4. settled 后注册的 then 仍然会运行。
  5. 缺省 handler 透传值或原因,handler 异常进入拒绝链。
  6. 返回值通过 thenable resolution procedure 处理,并拒绝 self-resolution。

它仍然不是 Promise polyfill,不应覆盖全局 Promise,也不应在生产代码替代原生 Promise。原作者“用很少代码解释异步链式调用”的教学动机保留;实现名称和免责声明用于防止读者把示例误当成完整 A+ 库。

原文归属

作者:ssh_晨曦时梦见兮。历史来源:掘金原文《最简实现Promise,支持异步链式调用(20行)》。原文链接仅用于保留历史出处,当前语义以本文列出的规范和官方文档为准。

参考链接

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

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

按分类浏览

10 COLLECTIONS