- 文档
- 教程
- 前端
【免费下载链接】zh.javascript.info
现代 JavaScript 教程(The Modern JavaScript Tutorial),以最新的 ECMAScript 规范为基准,通过简单但足够详细的内容,为你讲解从基础到高阶的 JavaScript 相关知识。
本篇文章基于开源仓库 zh.javascript.info 中「异步编程」章节的 Promise 基础篇 展开,系统地讲解 Promise 的诞生动机、构造器与 executor 的运行机制、state/result内部状态流转,以及.then/.catch/.finally三大消费方法的完整语义。全文结合仓库内 练习任务 与 示例源码,帮助你彻底理解「生产者代码」如何通过 Promise 把结果交给「消费者代码」,并掌握用 Promise 重构回调风格异步代码(如动态加载脚本)的实战能力。
Promise 解决的问题:生产者与消费者的解耦
在真实项目中,我们经常面对这样一个场景:一段代码(生产者代码)需要花费时间完成工作(例如通过网络加载数据),而另一些代码(消费者代码)希望在结果就绪的第一时间拿到它,并且可能有多个函数同时需要这个结果。
原文档用一个生动的类比解释了这一模型:一位顶尖歌手承诺在单曲发布的第一时间通知所有粉丝,粉丝们只需在「订阅列表」上留下邮箱即可。即使录音室失火导致新歌无法发布,订阅者也会及时收到通知——没有人被催促,也没有人错过消息。对应到编程中:
- 生产者代码(producing code)执行耗时任务,如同「歌手」;
- 消费者代码(consuming code)希望在结果完成时立刻获得它,如同「粉丝」;
- Promise是将两者连接起来的特殊 JavaScript 对象,如同「订阅列表」——生产者在任意时长后产出所承诺的结果,Promise 再把结果开放给所有已订阅的代码。
需要说明的是,这个类比并不完全精确:JavaScript 的 Promise 比简单的订阅列表功能更丰富,也有限制,但作为入门理解已经足够。
Promise 构造器与 executor
Promise 对象的构造器语法如下:
let promise = new Promise(function(resolve, reject) { // executor(生产者代码,"歌手") });传递给new Promise的函数被称为executor。它有两个关键行为:
- 自动执行:当
new Promise被创建时,executor 会立即自动运行; - 只写逻辑:executor 内部只包含最终应产出结果的生产者代码,而
resolve与reject这两个参数是由 JavaScript 引擎预先定义好的回调,我们无需(也不能)自行创建,只需要在合适的时机调用其中之一。
当 executor 获得结果——无论早还是晚——它应当调用以下回调之一:
resolve(value)—— 任务成功完成,携带结果value;reject(error)—— 出现错误,error通常是一个 error 对象。
state 与 result:Promise 的内部状态
由new Promise构造器返回的 promise 对象拥有两个内部属性:
| 属性 | 初始值 | 调用resolve(value)后 | 调用reject(error)后 |
|---|---|---|---|
state | "pending"(待定) | "fulfilled"(已成功) | "rejected"(已失败) |
result | undefined | value | error |
executor 最终会把 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 Promise); - executor 接受的
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":
必须牢记的四条关键规则
规则一:只有一个结果或一个 error
executor 只能调用一次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 与 catch
Promise 对象充当 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 procedures);finally会把结果或 error 传递给下一个合适的处理程序。例如结果从finally传递给了then:
new Promise((resolve, reject) => { setTimeout(() => resolve("value"), 2000) }) .finally(() => alert("Promise ready")) // 先触发 .then(result => alert(result)); // <-- .then 显示 "value"error 也会原样穿过finally抵达catch:
new 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 相对回调模式的优势
| Promise | callback |
|---|---|
Promise 允许我们按照自然顺序编码:先运行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毫秒后 resolve:
function delay(ms) { // 你的代码 } delay(3000).then(() => alert('runs after 3 seconds'));参考实现(solution.md):
function 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,从而把「动画完成」这一异步时刻暴露给.then:
function 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 是连接耗时「生产者代码」与「消费者代码」的订阅式对象;
- 构造器与 executor:
new Promise(executor)创建时 executor 自动运行,成功后调用resolve(value)、失败后调用reject(error); - 内部状态:
state从"pending"变为"fulfilled"/"rejected",result从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 相关知识。
相关推荐
千古前端图文教程: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),仅供参考