zh.javascript.info 教程:深入理解 JavaScript Promise 基础——从构造器、executor 到 then/catch/finally

发布时间:2026/10/8 23:30:14
zh.javascript.info 教程:深入理解 JavaScript Promise 基础——从构造器、executor 到 then/catch/finally
文档教程前端【免费下载链接】zh.javascript.info现代 JavaScript 教程The Modern JavaScript Tutorial以最新的 ECMAScript 规范为基准通过简单但足够详细的内容为你讲解从基础到高阶的 JavaScript 相关知识。项目地址https://gitcode.com/gh_mirrors/zh/zh.javascript.info点击查看免费下载本篇文章基于开源仓库 zh.javascript.info 中「异步编程」章节的 Promise 基础篇 展开系统地讲解 Promise 的诞生动机、构造器与 executor 的运行机制、state/result内部状态流转以及.then/.catch/.finally三大消费方法的完整语义。全文结合仓库内 练习任务 与 示例源码帮助你彻底理解「生产者代码」如何通过 Promise 把结果交给「消费者代码」并掌握用 Promise 重构回调风格异步代码如动态加载脚本的实战能力。Promise 解决的问题生产者与消费者的解耦在真实项目中我们经常面对这样一个场景一段代码生产者代码需要花费时间完成工作例如通过网络加载数据而另一些代码消费者代码希望在结果就绪的第一时间拿到它并且可能有多个函数同时需要这个结果。原文档用一个生动的类比解释了这一模型一位顶尖歌手承诺在单曲发布的第一时间通知所有粉丝粉丝们只需在「订阅列表」上留下邮箱即可。即使录音室失火导致新歌无法发布订阅者也会及时收到通知——没有人被催促也没有人错过消息。对应到编程中生产者代码producing code执行耗时任务如同「歌手」消费者代码consuming code希望在结果完成时立刻获得它如同「粉丝」Promise是将两者连接起来的特殊 JavaScript 对象如同「订阅列表」——生产者在任意时长后产出所承诺的结果Promise 再把结果开放给所有已订阅的代码。需要说明的是这个类比并不完全精确JavaScript 的 Promise 比简单的订阅列表功能更丰富也有限制但作为入门理解已经足够。Promise 构造器与 executorPromise 对象的构造器语法如下let promise new Promise(function(resolve, reject) { // executor生产者代码歌手 });传递给new Promise的函数被称为executor。它有两个关键行为自动执行当new Promise被创建时executor 会立即自动运行只写逻辑executor 内部只包含最终应产出结果的生产者代码而resolve与reject这两个参数是由 JavaScript 引擎预先定义好的回调我们无需也不能自行创建只需要在合适的时机调用其中之一。当 executor 获得结果——无论早还是晚——它应当调用以下回调之一resolve(value)—— 任务成功完成携带结果valuereject(error)—— 出现错误error通常是一个 error 对象。state 与 resultPromise 的内部状态由new Promise构造器返回的 promise 对象拥有两个内部属性属性初始值调用resolve(value)后调用reject(error)后statepending待定fulfilled已成功rejected已失败resultundefinedvalueerrorexecutor 最终会把 promise 迁移到fulfilled或rejected两者之一与初始的pending相对无论 promise 最终是 resolved 还是 rejected都统称为settled已敲定。一个完整的成功示例下面这段代码中executor 包含带setTimeout的「生产者代码」let promise new Promise(function(resolve, reject) { // 当 promise 被构造完成时自动执行此函数 // 1 秒后发出工作已经完成的信号并带有结果 done setTimeout(() resolve(done), 1000); });运行这段代码可以观察到两件事executor 被自动且立即调用通过new Promiseexecutor 接受的resolve和reject由 JavaScript 引擎预先定义我们只需要在准备好的时候调用其中之一。经过 1 秒的「处理」后executor 调用resolve(done)产生结果promise 对象状态随之改变一个失败示例下面则是 executor 以 error 拒绝 promise 的示例let promise new Promise(function(resolve, reject) { // 1 秒后发出工作已经完成的信号并带有 error setTimeout(() reject(new Error(Whoops!)), 1000); });对reject(...)的调用将 promise 的状态迁移至rejected必须牢记的四条关键规则规则一只有一个结果或一个 errorexecutor 只能调用一次resolve或一次reject任何状态的更改都是最终的。所有再对resolve和reject的调用都会被忽略let promise new Promise(function(resolve, reject) { resolve(done); reject(new Error(…)); // 被忽略 setTimeout(() resolve(…)); // 被忽略 });这一点在仓库练习 01-re-resolve 中被直接验证下面的代码输出1而非2因为第二个resolve调用会被忽略只有第一次对reject/resolve的调用才会被处理let promise new Promise(function(resolve, reject) { resolve(1); setTimeout(() resolve(2), 1000); }); promise.then(alert);另外resolve/reject只需要一个参数或不包含任何参数额外的参数会被忽略。规则二建议以 Error 对象 reject如果出现问题executor 应当调用reject。虽然可以像resolve一样传入任何类型的参数但建议使用Error对象或继承自Error的对象——原因在后续学习错误处理链.catch与throw的配合时会很快变得显而易见。规则三resolve/reject 可以立即进行executor 通常是异步执行某些操作、一段时间后再调用resolve/reject但这并非必须let promise new Promise(function(resolve, reject) { // 不花时间去做这项工作 resolve(123); // 立即给出结果123 });例如当我们开始一个任务随后发现一切都已经完成并已被缓存时就可能出现这种情况。此时我们立即就拥有了一个 resolved 的 promise。规则四state 和 result 都是内部的Promise 对象的state和result属性都是内部的我们无法直接访问它们只能通过.then/.catch/.finally方法间接消费。下面就来介绍这些方法。消费者then 与 catchPromise 对象充当 executor生产者和消费函数粉丝之间的连接。消费函数通过.then和.catch方法注册。then同时注册成功与失败处理.then是最重要、最基础的方法语法如下promise.then( function(result) { /* 处理成功结果 */ }, function(error) { /* 处理 error */ } );第一个参数promise resolved 并接收到结果后执行第二个参数promise rejected 并接收到 error 后执行。对成功 resolved 的 promise 做出反应let promise new Promise(function(resolve, reject) { setTimeout(() resolve(done!), 1000); }); // resolve 运行 .then 中的第一个函数 promise.then( result alert(result), // 1 秒后显示 done! error alert(error) // 不运行 );对 rejected 的 promise 做出反应let promise new Promise(function(resolve, reject) { setTimeout(() reject(new Error(Whoops!)), 1000); }); // reject 运行 .then 中的第二个函数 promise.then( result alert(result), // 不运行 error alert(error) // 1 秒后显示 Error: Whoops! );如果只对成功完成感兴趣可以只传一个函数参数let promise new Promise(resolve { setTimeout(() resolve(done!), 1000); }); promise.then(alert); // 1 秒后显示 done!catch只处理 error如果只对 error 感兴趣可以使用null作为第一个参数.then(null, errorHandlingFunction)更简洁的写法是.catch(errorHandlingFunction)二者完全等价let promise new Promise((resolve, reject) { setTimeout(() reject(new Error(Whoops!)), 1000); }); // .catch(f) 与 promise.then(null, f) 一样 promise.catch(alert); // 1 秒后显示 Error: Whoops!清理finally就像try {...} catch {...}中的finally子句一样promise 也有finally。调用.finally(f)类似于.then(f, f)——当 promise settled无论 resolve 还是 reject时f就会执行。它的典型用途是执行清理/终结操作停止加载指示器、关闭不再需要的连接等。new Promise((resolve, reject) { /* 做一些需要时间的事之后调用可能会 resolve 也可能会 reject */ }) // 在 promise 为 settled 时运行无论成功与否 .finally(() stop loading indicator) // 所以加载指示器始终会在我们继续之前停止 .then(result show result, err show error)但请注意finally(f)并不完全是then(f, f)的别名两者有重要区别finally处理程序没有参数——在finally中我们不知道 promise 是成功还是失败这没关系因为它的任务通常是执行「常规」的完成程序finalizing proceduresfinally会把结果或 error 传递给下一个合适的处理程序。例如结果从finally传递给了thennew Promise((resolve, reject) { setTimeout(() resolve(value), 2000) }) .finally(() alert(Promise ready)) // 先触发 .then(result alert(result)); // -- .then 显示 valueerror 也会原样穿过finally抵达catchnew Promise((resolve, reject) { throw new Error(error); }) .finally(() alert(Promise ready)) // 先触发 .catch(err alert(err)); // -- .catch 显示这个 errorfinally处理程序不应返回任何内容——返回值会被默认忽略。唯一的例外是当finally处理程序抛出 error 时这个 error而非之前的结果会转到下一个处理程序。总结finally处理程序拿不到前一个处理程序的结果它没有参数结果被传递给了下一个合适的处理程序如果finally返回了一些内容这些内容会被忽略当finally抛出 error 时执行将转到最近的 error 处理程序。只要把finally正确用于「常规清理」这些特性都会非常有用。附加处理程序settled 之后也能订阅如果 promise 处于 pending 状态.then/catch/finally处理程序会等待它的结果但有时候当我们添加处理程序时promise 可能已经 settled 了——此时这些处理程序会立即执行// 这个 promise 在被创建后立即变为 resolved 状态 let promise new Promise(resolve resolve(done!)); promise.then(alert); // done!立刻显示这使 promise 比现实生活中的「订阅列表」强大得多如果歌手已经发布了单曲之后才在订阅列表上注册的人很可能收不到通知——现实生活的订阅必须在活动开始前进行而 Promise 则非常灵活我们可以随时添加处理程序如果结果已经存在它们就会立刻执行。实战重构用 Promise 重写 loadScript上一章回调基础中我们见过基于回调的loadScript函数它通过script.onload/script.onerror通知结果function loadScript(src, callback) { let script document.createElement(script); script.src src; script.onload () callback(null, script); script.onerror () callback(new Error(Script load error for ${src})); document.head.append(script); }现在用 Promise 重写它。新函数不再需要回调而是创建并返回一个在加载完成时 resolve 的 promise 对象外部代码用.then添加处理程序订阅函数function loadScript(src) { return new Promise(function(resolve, reject) { let script document.createElement(script); script.src src; script.onload () resolve(script); script.onerror () reject(new Error(Script load error for ${src})); document.head.append(script); }); }仓库中 head.html 保留了这份 Promise 版loadScript的完整实现供配套练习页面直接引用。使用方法如下let promise loadScript(https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js); promise.then( script alert(${script.src} is loaded!), error alert(Error: ${error.message}) ); promise.then(script alert(Another handler...));Promise 相对回调模式的优势PromisecallbackPromise 允许我们按照自然顺序编码先运行loadScript再用.then处理结果。调用loadScript(script, callback)时必须预先准备好callback函数——即在调用loadScript之前就要知道如何处理结果。可以按需在 promise 上多次调用.then每次调用都像在「订阅列表」中添加一个新的订阅函数详见下一章 Promise 链。只能有一个回调。因此promise 提供了更好的代码流与灵活性。更多内容将在 Promise 链 与 Promise 错误处理 中展开。仓库配套练习从原理到落地本小节对应的三个练习任务覆盖了「单一结果」「无参 resolve」「异步 resolve 与 DOM 集成」三类典型场景直接印证上文所述的 Promise 语义。练习一用 promise 重新解决任务task.md下面的代码会输出什么let promise new Promise(function(resolve, reject) { resolve(1); setTimeout(() resolve(2), 1000); }); promise.then(alert);答案solution.md输出1。第二个resolve调用被忽略因为只有第一次对reject/resolve的调用才会被处理进一步的调用都会被忽略——这正是「单一结果」规则的实际验证。练习二基于 promise 的延时任务task.md内建函数setTimeout使用回调请创建基于 promise 的替代方案——函数delay(ms)返回一个 promise在ms毫秒后 resolvefunction delay(ms) { // 你的代码 } delay(3000).then(() alert(runs after 3 seconds));参考实现solution.mdfunction delay(ms) { return new Promise(resolve setTimeout(resolve, ms)); } delay(3000).then(() alert(runs after 3 seconds));注意此任务中resolve是不带参数调用的——我们不从delay中返回任何值只是确保延迟即可。这也印证了「resolve/reject只需要一个参数或不包含任何参数」的规则。练习三带有 promise 的圆形动画任务task.md要求重写 CSS 动画章节 animate-circle-callback 任务 的解决方案中的showCircle函数使其返回 promise 而非接受回调新用法如下showCircle(150, 150, 100).then(div { div.classList.add(message-ball); div.append(Hello, world!); });仓库配套页面 solution.view/index.html 给出了完整实现。核心思路是promise 在transitionend事件触发时才 resolve从而把「动画完成」这一异步时刻暴露给.thenfunction showCircle(cx, cy, radius) { let div document.createElement(div); div.style.width 0; div.style.height 0; div.style.left cx px; div.style.top cy px; div.className circle; document.body.append(div); return new Promise(resolve { setTimeout(() { div.style.width radius * 2 px; div.style.height radius * 2 px; div.addEventListener(transitionend, function handler() { div.removeEventListener(transitionend, handler); resolve(div); }); }, 0); }); }这个例子展示了 Promise 更进阶的用法executor 内部可以组合setTimeout与 DOM 事件把任意异步时点这里是 CSS 过渡动画结束封装成 promise 的 resolve 时机从而让调用方用统一的.then风格消费各种异步结果。小结本篇文章完整覆盖了 Promise 基础的全部核心内容生产者/消费者模型Promise 是连接耗时「生产者代码」与「消费者代码」的订阅式对象构造器与 executornew Promise(executor)创建时 executor 自动运行成功后调用resolve(value)、失败后调用reject(error)内部状态state从pending变为fulfilled/rejectedresult从undefined变为value/error且只能迁移一次消费方法.then同时注册成功/失败处理.catch等价于.then(null, f).finally用于无参数的常规清理并透传结果实战能力通过loadScript的重构对比了 promise 与 callback 的代码流差异并通过仓库配套的三个练习验证了「单一结果」「无参 resolve」「异步 resolve」等关键语义。下一章 Promise 链 将介绍如何在多个异步步骤之间串联 promise请继续阅读。赞分享文档教程前端【免费下载链接】zh.javascript.info现代 JavaScript 教程The Modern JavaScript Tutorial以最新的 ECMAScript 规范为基准通过简单但足够详细的内容为你讲解从基础到高阶的 JavaScript 相关知识。项目地址https://gitcode.com/gh_mirrors/zh/zh.javascript.info点击查看免费下载相关推荐千古前端图文教程Promise 实例方法 then、catch、finally 全解千古前端图文教程Promise 实例方法 then、catch、finally 全解 本文是「千古前端图文教程」异步编程系列的核心篇章系统讲解 Promis文档教程前端JavaScript 教程深入理解 Promise 基础JavaScript 教程深入理解 Promise 基础 你是否曾经在JavaScript开发中遇到过回调地狱Callback Hell代码层层嵌套文档教程前端Parcel 2 零配置原生 Web 应用模板从本地开发热更新到 Vercel 部署的完整实践Parcel 2 零配置原生 Web 应用模板从本地开发热更新到 Vercel 部署的完整实践 本文以当前仓库中的 framework boilerplate示例工程前端后端创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考