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

显示模式

登录
ARCHIVE DOCUMENTJS

Promise 必知必会(十道题)

所属馆藏
JavaScript
文件格式
Markdown
原始路径
JavaScript/52-Promise 必知必会(十道题)
本文目录14 个章节
  1. 预备知识:状态、解决和兑现
  2. 题目一:构造器同步,then 异步
  3. 题目二:then 返回新的 Promise
  4. 题目三:第一次状态转换生效
  5. 题目四:每个 then 返回一环
  6. 题目五:同一个 Promise 可以注册多个 then
  7. 题目六:返回 Error 对象不会自动拒绝
  8. 题目七:返回自身会形成链式循环
  9. 题目八:非函数处理器会值穿透
  10. 题目九:then 的第二个参数捕获范围有限
  11. 题目十:Node.js 中 nextTick、Promise job 与 setImmediate
  12. 十一、Promise 静态方法补充
  13. 十二、常见结论
  14. 参考资料

Promise 必知必会(十道题)

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

本文保留原文的十道题和解题顺序,修正旧版 Node.js 的 UnhandledPromiseRejectionWarning、粘连输出、process.nextTick 分类和环境依赖的运行结果,并补充 Promise 的状态、微任务、thenable、静态组合方法和错误处理边界。示例中涉及 process 的代码请使用 Node.js 运行;浏览器没有 process.nextTick

原文:Promise 必知必会(十道题)

预备知识:状态、解决和兑现

Promise 常被概括为三种状态:

  • pending:等待中;
  • fulfilled:已兑现,有结果值;
  • rejected:已拒绝,有拒绝原因。

状态只能从 pending 变为 fulfilledrejected,并且只能变化一次。规范中还使用“resolved(已解决)”描述 Promise 已经锁定到某个结果,包括它正在等待另一个 pending Promise 的情况,因此 resolved 不完全等同于 fulfilled:

const adopted = new Promise(resolve => {
  resolve(new Promise(innerResolve => {
    setTimeout(() => innerResolve('done'), 10)
  }))
})

// adopted 已经采用了内层 Promise 的结果,但要等内层完成后才 fulfilled
adopted.then(console.log) // done

Promise 构造器中的 executor 会同步执行;then/catch 的处理函数会异步执行,通常排入 ECMAScript Promise job/microtask。

题目一:构造器同步,then 异步

const promise = new Promise(resolve => {
  console.log(1)
  resolve()
  console.log(2)
})

promise.then(() => {
  console.log(3)
})

console.log(4)

输出顺序:

1
2
4
3

解释:调用 new Promise 时 executor 立即运行;resolve() 只改变 Promise 状态,不会同步执行已经注册或随后注册的 then 处理函数。then 回调会在当前同步代码结束后执行。

题目二:then 返回新的 Promise

const promise1 = new Promise(resolve => {
  setTimeout(() => resolve('success'), 1000)
})

const promise2 = promise1.then(() => {
  throw new Error('error!!!')
})

console.log(promise1)
console.log(promise2)

promise2.catch(error => {
  console.log(error.message) // error!!!
})

setTimeout(() => {
  console.log(promise1) // fulfilled with success
  console.log(promise2) // rejected with Error
}, 2000)

刚创建时两个 Promise 通常都是 pending;约一秒后 promise1 fulfilled,promise2 因处理函数抛错而 rejected。重点是:

  • promise2 不是 promise1
  • 每次调用 then 都返回新的 Promise;
  • 处理函数抛出的异常会拒绝返回的 Promise;
  • 应该对可能拒绝的 Promise 注册处理器。

原文记录的 UnhandledPromiseRejectionWarning 是旧版 Node.js 的输出格式。现代 Node.js 对未处理拒绝的默认行为和诊断文本可能随版本、启动参数变化,未处理 rejection 可能导致进程以非零状态退出。不要把警告文本当作 Promise 语义;及时 catch 才是正确做法。

题目三:第一次状态转换生效

const promise = new Promise((resolve, reject) => {
  resolve('success1')
  reject('error')
  resolve('success2')
})

promise
  .then(value => {
    console.log('then:', value)
  })
  .catch(error => {
    console.log('catch:', error)
  })

输出:

then: success1

第一次调用 resolve 把 Promise 从 pending 变为 fulfilled,后续 rejectresolve 都不会改变它。这个“一次性”规则也适用于 thenable 解析过程中先调用的 resolve/reject

题目四:每个 then 返回一环

Promise.resolve(1)
  .then(value => {
    console.log(value)
    return 2
  })
  .catch(() => 3)
  .then(value => {
    console.log(value)
  })

输出:

1
2

第一个 then 返回 2,所以它返回的 Promise fulfilled 为 2;由于没有发生 rejection,中间的 catch 不会执行,后一个 then 收到 2

Promise 的链式调用不是通过 return this 实现的,而是每次创建新的 Promise 并把处理函数的结果解析到新 Promise 中。

题目五:同一个 Promise 可以注册多个 then

const promise = new Promise(resolve => {
  setTimeout(() => {
    console.log('once')
    resolve('success')
  }, 1000)
})

const start = Date.now()
promise.then(value => {
  console.log('first:', value, Date.now() - start)
})
promise.then(value => {
  console.log('second:', value, Date.now() - start)
})

典型输出类似:

once
first: success 1000
second: success 1000

实际毫秒数取决于计时器和系统调度,不能写死为 10051007。executor 只执行一次,但同一个 Promise 可以注册多个处理函数;这些处理函数按照注册顺序处理,且各自返回的 Promise 属于独立链。

题目六:返回 Error 对象不会自动拒绝

Promise.resolve()
  .then(() => {
    return new Error('error object')
  })
  .then(value => {
    console.log('then:', value.message) // error object
  })
  .catch(error => {
    console.log('catch:', error.message)
  })

输出的是 then: error objectError 只是一个普通对象,return 一个对象表示 fulfilled:

Promise.resolve()
  .then(() => {
    throw new Error('thrown error')
  })
  .catch(error => {
    console.log('catch:', error.message) // thrown error
  })

Promise.resolve()
  .then(() => Promise.reject(new Error('rejected error')))
  .catch(error => {
    console.log('catch:', error.message) // rejected error
  })

处理函数的返回值会按照 Promise.resolve 的规则解析:普通值(包括 Error 对象)fulfilled,抛出异常或返回 rejected Promise 才会进入后续拒绝路径。

题目七:返回自身会形成链式循环

let promise

promise = Promise.resolve().then(() => promise)

promise.catch(error => {
  console.log(error.name) // TypeError
})

then 返回的 Promise 不能再等待自己,否则它永远无法完成。原生实现会检测这个直接自引用并拒绝,错误文本可能因 Node.js/浏览器而不同。

原文用无限递归的 process.nextTick 类比循环:

// 不要运行无限版本,它会持续占用 Node.js 的 nextTick 队列并阻塞 I/O。
let count = 0
function tick() {
  if (count++ < 3) process.nextTick(tick)
}
process.nextTick(tick)

无限 nextTick 递归不是 Promise 自引用的实现机制,只能说明“持续排队可能让其他任务得不到机会”。两者都应避免混为一谈。

题目八:非函数处理器会值穿透

Promise.resolve(1)
  .then(2)
  .then(Promise.resolve(3))
  .then(value => {
    console.log(value)
  })

输出:

1

then 的参数只有在是函数时才会被当作处理器:

  • 成功处理器不是函数:使用 value => value
  • 失败处理器不是函数:使用 reason => { throw reason }

Promise.resolve(3) 这个表达式虽然会先创建一个 Promise,但它作为 then 参数不是函数,因此不会被执行,也不会改变值穿透结果。

题目九:then 的第二个参数捕获范围有限

Promise.resolve()
  .then(
    () => {
      throw new Error('error')
    },
    error => {
      console.log('fail1:', error)
    }
  )
  .catch(error => {
    console.log('fail2:', error.message)
  })

输出:

fail2: error

then(onFulfilled, onRejected) 的两个处理器都只处理前一个 Promise的状态:

  • onRejected 不能捕获同一次 thenonFulfilled 中抛出的异常;
  • 该异常会拒绝 then 返回的新 Promise;
  • 后续的 catch 可以捕获它。

这也是更常见地使用 .then(onFulfilled).catch(onRejected) 的原因:

fetch('/api/data')
  .then(response => response.json())
  .then(data => render(data))
  .catch(error => {
    console.error('请求或处理失败', error)
  })

function render(data) {
  console.log(data)
}

如果明确只想处理“上一步 Promise 的拒绝”,可以使用 then(undefined, onRejected),但它不会捕获同一个 then 的成功处理器抛错。

题目十:Node.js 中 nextTick、Promise job 与 setImmediate

// 请保存为 CommonJS 文件(例如 event-loop.cjs)运行
process.nextTick(() => {
  console.log('nextTick')
})

Promise.resolve().then(() => {
  console.log('then')
})

setImmediate(() => {
  console.log('setImmediate')
})

console.log('end')

在常见的 Node.js CommonJS 启动场景中,通常看到:

end
nextTick
then
setImmediate

需要区分三个概念:

  • process.nextTick 使用 Node.js 单独的 next tick 队列,通常在当前 JavaScript 调用栈结束后、继续事件循环前优先处理;
  • Promise.then 使用 ECMAScript Promise job/microtask 队列;
  • setImmediate 在 Node.js 事件循环的 check 阶段执行。

Node.js 的 CommonJS 与 ESM 启动路径、不同事件循环阶段、I/O 回调位置和版本都会影响具体顺序。尤其在 ESM 中,模块评估本身通过 Promise job 运行,Promise.thenprocess.nextTick 的相对观察顺序可能和 CommonJS 不同。不要把 nextTick 简单写成“和 Promise 完全相同的 microtask”。

setTimeout(..., 0)setImmediate() 在主模块中谁先执行也不应写死;在 I/O 回调内部通常更容易观察到 setImmediate 先于计时器。需要验证顺序时,应注明 Node.js 版本、模块格式和代码所在事件循环阶段。

十一、Promise 静态方法补充

原文主要讨论 allrace,现代 Promise 还提供:

const tasks = [
  Promise.resolve('a'),
  Promise.reject(new Error('failed'))
]

Promise.all(tasks).catch(error => console.log(error.message)) // failed

Promise.allSettled(tasks).then(results => {
  console.log(results.map(result => result.status)) // ['fulfilled', 'rejected']
})

Promise.any([
  Promise.reject(new Error('first')),
  Promise.resolve('success')
]).then(console.log) // success

Promise.race([
  new Promise(resolve => setTimeout(() => resolve('slow'), 20)),
  Promise.resolve('fast')
]).then(console.log) // fast
  • Promise.all:全部 fulfilled 才成功,任一 rejected 就拒绝,结果顺序与输入一致;
  • Promise.allSettled:等待全部结束,返回每项状态;
  • Promise.any:第一个 fulfilled 成功,全部 rejected 时以 AggregateError 拒绝;
  • Promise.race:第一个 settled 决定结果,不会自动取消其他任务。

超时控制只让返回的 Promise 先失败,不会自动取消底层网络请求;网络请求需要配合 AbortController

async function fetchWithTimeout(url, timeout = 5000) {
  const controller = new AbortController()
  const timer = setTimeout(() => controller.abort(), timeout)

  try {
    const response = await fetch(url, { signal: controller.signal })
    return response
  } finally {
    clearTimeout(timer)
  }
}

十二、常见结论

  1. executor 同步执行,Promise 处理器异步执行;
  2. Promise 状态只能从 pending 变化一次;
  3. then/catch 每次返回新 Promise;
  4. 返回 Error 对象是 fulfilled,抛出 Error 或返回 rejected Promise 才是 rejected;
  5. 非函数处理器会发生值穿透;
  6. 同一个 Promise 可以有多个监听者,监听者之间不会互相等待;
  7. then 返回 thenable 时需要递归采用其结果;
  8. Promise 自引用会导致 TypeError;
  9. Node.js process.nextTick 不是浏览器 Promise microtask 的同义词;
  10. race 和超时 Promise 不会自动取消已经开始的任务;
  11. 未处理 rejection 的输出和进程行为会随运行时版本变化,应显式处理错误。

参考资料

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

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

按分类浏览

10 COLLECTIONS