Promise 从入门到实战:同步异步、回调地狱、then/catch/finally 全解

同学们好,我是 Eugene(尤金),一个拥有多年中后台开发经验的前端工程师~

(Eugene 发音很简单,/juːˈdʒiːn/,大家怎么顺口怎么叫就好)

你是否也有过:明明学过很多技术,一到关键时候却讲不出来、甚至写不出来?

你是否也曾怀疑自己,是不是太笨了,明明感觉会,却总差一口气?

就算想沉下心从头梳理,可工作那么忙,回家还要陪伴家人。

一天只有24小时,时间永远不够用,常常感到力不从心。

技术行业,本就是逆水行舟,不进则退。

如果你也有同样的困扰,别慌。

从现在开始,跟着我一起心态归零 ,利用碎片时间,来一次彻彻底底的基础扫盲

这一次,我们一起慢慢来,扎扎实实变强。

不搞花里胡哨的理论堆砌,只分享看得懂、用得上的前端干货,

咱们一起稳步积累,真正摆脱"面向搜索引擎写代码"的尴尬。


1. 前言:Promise 到底解决什么问题?

简单粗暴一点的说,Promise 就是 JavaScript 中专门解决异步代码问题的工具,它最核心的作用,就是让原本混乱无序的异步代码,按照我们想要的顺序执行,彻底解决异步嵌套带来的可读性差、维护困难等问题,是前端异步编程的基础核心知识点。

2. 同步代码与异步代码:JavaScript 执行顺序解析

要学好 Promise,首先得分清什么是同步代码、什么是异步代码------这是理解 Promise 作用的前提,也是前端异步编程的基础。

  • 同步代码:严格按顺序执行,就像排队办事,只有前一个人办完,下一个人才能开始,前一条代码执行完毕后,才会执行后一条代码,比如 1、2、3、4、5 依次执行,不会出现跳跃。
  • 异步代码:可以不按顺序执行的代码,就像排队时有人临时去取东西,不用等他回来,后面的人可以继续办事,比如执行顺序可能是 2、1、3、4、6、5。常见的 JavaScript 异步场景有:网络请求、定时器、文件读写等。

我们直接上代码,直观感受异步代码的执行逻辑:

js 复制代码
setTimeout(() => {
  
}, 1000)

↑这是一个 JavaScript 定时器,也是最常见的异步代码之一,它的作用是将包裹在方法体内的代码,延迟 1000 毫秒(1秒)后再执行。

3. 异步代码的特性:非阻塞与执行顺序验证

我们通过一个完整的定时器案例,验证异步代码的执行顺序,同时理解异步代码"非阻塞"的核心特性:

js 复制代码
console.log('任务1', moment(new Date()).format('HH:mm:ss'))

setTimeout(() => {

  console.log('任务2', moment(new Date()).format('HH:mm:ss'))

}, 1000)

// 补充说明:

// moment(new Date()).format('HH:mm:ss') 是用于输出当前执行时间的代码,

// 初学者如果不明白 moment 是什么、复制代码报错,可直接删除该部分,只输出 '任务1、任务2' 即可,不影响学习。

// 感兴趣的同学可自行搜索 moment.js 的使用方法,用于日期时间格式化。

执行结果如下(截图对应原文示例):

从截图可以清晰看到,任务2 比任务1 的打印时间晚了1秒,这就是异步代码的执行逻辑------定时器中的代码不会阻塞后续代码执行,而是延迟执行。

我们再调整一下代码顺序,进一步验证异步代码的特性:

js 复制代码
setTimeout(() => {
  console.log('任务2', moment(new Date()).format('HH:mm:ss'))
}, 1000)

console.log('任务1', moment(new Date()).format('HH:mm:ss'))

按照代码自上而下的顺序,很多初学者会认为:应该先等待1秒,输出任务2,再输出任务1。但实际执行结果并非如此,截图如下:

控制台输出显示:代码跳过了异步的任务2,先执行了同步的任务1,1秒后才执行任务2。这就证实了:定时器是典型的异步代码,不会按照自上而下的顺序执行,也不会阻塞后续同步代码的执行

这里补充一个通俗类比,帮大家理解异步代码"非阻塞"的好处:

假设我们是交通参与者,开车行驶在路上,前方发生交通事故。如果有辅道可以绕行,我们肯定不会等到事故处理完再通行------这就是异步的逻辑,不会因为一个"任务"(事故处理)阻塞整体"流程"(通行)。如果必须等待事故处理完再通行,就会造成交通瘫痪,对应到代码中,就是"阻塞",会导致页面卡顿、响应缓慢。

4. 回调地狱:异步嵌套的痛点的详解

虽然异步代码是非阻塞的,但在实际开发中,我们经常会遇到"异步任务需要按顺序执行"的场景------这时候,单纯的异步嵌套就会产生"回调地狱",也是 Promise 要解决的核心痛点。

还是用交通场景类比:如果我们是交警,接到调度中心任务,先处理A路段事故,处理完再处理B路段事故(B任务必须等待A任务完成)。其他社会车辆可以正常绕行(不阻塞),但交警的两个任务必须同步执行。我们用代码还原这个场景:

js 复制代码
console.log('社会车辆1')
console.log('社会车辆2')
setTimeout(() => {
  console.log('交警处理事故1', moment(new Date()).format('HH:mm:ss'))
  setTimeout(() => {
    console.log('交警处理事故2', moment(new Date()).format('HH:mm:ss'))
  }, 1000)
}, 1000)
console.log('社会车辆3')
console.log('社会车辆4')

执行结果截图:

从输出可以看到:社会车辆1、2、3、4(同步代码)先行执行,交警处理完事故1(第一个异步任务)后,才执行事故2(第二个异步任务),符合"异步任务按顺序执行"的需求。

但问题来了:如果需要处理的异步任务增多(比如6个事故),代码就会嵌套一层又一层,形成"回调地狱":

js 复制代码
console.log('社会车辆1')
console.log('社会车辆2')
setTimeout(() => {
  console.log('交警处理事故1', moment(new Date()).format('HH:mm:ss'))
  setTimeout(() => {
    console.log('交警处理事故2', moment(new Date()).format('HH:mm:ss'))
    setTimeout(() => {
      console.log('交警处理事故3', moment(new Date()).format('HH:mm:ss'))
      setTimeout(() => {
        console.log('交警处理事故4', moment(new Date()).format('HH:mm:ss'))
        setTimeout(() => {
          console.log('交警处理事故5', moment(new Date()).format('HH:mm:ss'))
          setTimeout(() => {
            console.log('交警处理事故6', moment(new Date()).format('HH:mm:ss'))
          }, 1000)
        }, 1000)
      }, 1000)
    }, 1000)
  }, 1000)
}, 1000)
console.log('社会车辆3')
console.log('社会车辆4')

这种嵌套写法的致命问题的是:可读性极差、维护困难。简单的6个任务就已经嵌套6层,一旦某个任务需要修改逻辑、排查bug,就需要层层查找,非常繁琐------这就是"回调地狱",也是 JavaScript 异步编程中最头疼的问题。而 Promise 的出现,就是为了彻底规避这个问题,让异步代码变得简洁、优雅、易维护。

5. Promise 登场:解决回调地狱的优雅方案

Promise 是 JavaScript 内置的异步编程对象,专门用于处理异步任务的顺序执行,告别回调嵌套。它的基础语法非常简单,核心就是通过"链式调用"替代"嵌套调用",让异步代码的执行顺序清晰可见。

Promise 的基础语法如下:

js 复制代码
new Promise()

↑这就是 Promise 的基本结构,它本质是一个构造函数,接收一个函数作为参数,这个函数内部又接收两个内置参数:resolve 和 reject,由 Promise 对象自动传入:

js 复制代码
new Promise((resolve, reject) => {})

我们用通俗的方式解释这两个参数,结合前端常见场景(登录验证)理解更易上手:

  • resolve:直译"解决、决定",在异步任务执行成功时调用,可传递成功的结果(比如登录成功的用户信息)。
  • reject:直译"拒绝",在异步任务执行失败时调用,可传递失败的原因(比如登录时用户名/密码错误)。

简单总结:resolve = 异步成功,reject = 异步失败,两者都是用于标记异步任务的执行状态,并传递结果。

6. Promise 核心:状态与 resolve/reject 用法

Promise 有一个核心特性------状态管理,它的状态决定了异步任务的执行结果,且状态一旦改变,就无法再修改。Promise 有三种状态:

  • pending:默认状态,异步任务未执行完成(等待中),此时 resolve 和 reject 都未被调用。
  • fulfilled:成功状态,异步任务执行成功,调用 resolve 后,状态从 pending 变为 fulfilled。
  • rejected:失败状态,异步任务执行失败,调用 reject 后,状态从 pending 变为 rejected。

我们通过代码验证这三种状态,先打印一个未调用 resolve/reject 的 Promise 对象:

js 复制代码
const promiseObj = new Promise((resolve, reject) => {})

console.log(promiseObj)

执行结果截图:

从截图可以看到,Promise 的状态(PromiseState)是 pending,说明异步任务处于等待中,未执行成功或失败。

接下来,调用 resolve,查看状态变化:

js 复制代码
const promiseObj = new Promise((resolve, reject) => {
  resolve()
})
console.log(promiseObj)

执行结果截图:

状态从 pending 变为 fulfilled,说明异步任务执行成功,符合我们对 resolve 的定义。

再调用 reject,查看状态变化:

js 复制代码
const promiseObj = new Promise((resolve, reject) => {
  reject()
})
console.log(promiseObj)

执行结果截图:

状态从 pending 变为 rejected,说明异步任务执行失败,符合我们对 reject 的定义。

7. Promise 三大API:then、catch、finally 实战

Promise 对象内置了三个核心方法:then、catch、finally,用于接收异步任务的执行结果(成功/失败),也是实现链式调用的关键。我们结合登录场景,通俗讲解这三个方法的用法,再通过代码验证。

核心用法总结(登录场景类比):

  • then() 方法:异步任务执行成功(调用 resolve)时触发,接收 resolve 传递的成功数据。比如登录成功,就进入 then 方法,执行后续操作(如跳转首页)。
  • catch() 方法:异步任务执行失败(调用 reject)时触发,接收 reject 传递的失败原因。比如登录失败(用户名/密码错误),就进入 catch 方法,提示错误信息。
  • finally() 方法:无论异步任务成功还是失败,都会触发,不依赖 resolve 和 reject。比如登录操作结束后,无论成功与否,都关闭加载弹窗。

验证1:调用 resolve,触发 then 和 finally

js 复制代码
const promiseObj = new Promise((resolve, reject) => {
  resolve('身份认证通过!')
})
promiseObj
  .then((data) => { //data就是resolve传过来的内容 名称没有规定自定义即可
    console.log(data)
  })
  .catch((error) => {
    console.log(error)
  })
  .finally(() => {
    console.log('我是finally')
  })

执行结果截图:

结论:调用 resolve 后,代码先执行 then 方法(打印成功信息),再执行 finally 方法,catch 方法未触发,符合预期。

验证2:调用 reject,触发 catch 和 finally

js 复制代码
const promiseObj = new Promise((resolve, reject) => {
  reject('身份认证失败!')
})
promiseObj
  .then((data) => { 
    console.log(data)
  })
  .catch((error) => { //error就是reject传过来的内容  名称没有规定自定义即可
    console.log(error)
  })
  .finally(() => {
    console.log('我是finally')
  })

执行结果截图:

结论:调用 reject 后,代码先执行 catch 方法(打印失败原因),再执行 finally 方法,then 方法未触发,符合预期。

8. Promise 链式调用:彻底告别回调地狱

掌握了 Promise 的状态和三大API后,我们就可以用它的链式调用,解决前面提到的回调地狱问题。这里需要注意一个关键知识点:Promise 本身是同步的,但 then、catch、finally 方法中的回调函数是异步的,因此我们无需再用定时器,就能实现异步任务的顺序执行。

我们用 Promise 重写"交警处理多个事故"的案例,对比回调嵌套,看看链式调用的优雅之处:

js 复制代码
console.log('社会车辆1')
console.log('社会车辆2')
const promiseObj = new Promise((resolve, reject) => {
  resolve('交警处理事故1')
})
promiseObj
  .then((data) => {
    console.log(data)
    return new Promise((resolve, reject) => {
      resolve('交警处理事故2')
    })
  })
  .then((data) => {
    console.log(data)
    return new Promise((resolve, reject) => {
      resolve('交警处理事故3')
    })
  })
  .then((data) => {
    console.log(data)
    return new Promise((resolve, reject) => {
      resolve('交警处理事故4')
    })
  })
  .then((data) => {
    console.log(data)
  })
  .catch()
console.log('社会车辆3')
console.log('社会车辆4')

执行结果截图:

有同学会说:这也有嵌套啊?没错,但这种嵌套只有一层,无论有多少个异步任务,都只会嵌套一层,整体结构是"平铺式"的,而非"嵌套式"的,可读性大大提升。我们把链式调用简化来看,更直观:

js 复制代码
promiseObj.then().then().then().then().then().then().catch()

这种平铺的链式结构,无论多少个异步任务,都能清晰看到执行顺序,彻底告别回调地狱的嵌套乱象。

补充一个进阶用法:给每个 then 方法单独设置错误捕获(then 方法可接收两个参数,第二个参数用于捕获当前 then 的错误):

截图参考(对应原文示例):

从截图可以看到,then 方法的第一个参数是"成功回调",第二个参数是"当前 then 的错误回调",结构如下:

js 复制代码
promiseObj
  .then((data) => {}, (error) => {})
  .then((data) => {}, (error) => {})
  .then((data) => {}, (error) => {})
  .then((data) => {}, (error) => {})
  .then((data) => {}, (error) => {})

我们用代码验证这种用法,在"交警处理事故3"时触发 reject,看看错误捕获的效果:

js 复制代码
const promiseObj = new Promise((resolve, reject) => {
  resolve('交警处理事故1')
})
console.log('社会车辆1')
console.log('社会车辆2')
promiseObj
  .then(
    (data) => {
      console.log(data)
      return new Promise((resolve, reject) => {
        resolve('交警处理事故2')
      })
    },
    (error) => {
      console.log(error)
    }
  )
  .then(
    (data) => {
      console.log(data)
      let errorBtn = false
      return new Promise((resolve, reject) => {
        if (errorBtn) {
          resolve('交警处理事故3')
        } else {
          reject('交通事故处理时候遇到特殊状况')
        }
      })
    },
    (error) => {
      console.log(error)
    }
  )
  .then(
    (data) => {
      console.log(data)
      return new Promise((resolve, reject) => {
        resolve('交警处理事故4')
      })
    },
    (error) => {
      console.log(error)
    }
  )
  .then()
 
console.log('社会车辆3')
console.log('社会车辆4')

执行结果截图:

结论:当第二个 then 中触发 reject 后,代码会执行当前 then 的错误回调(打印错误信息),且后续的 then 方法不再执行,实现了精准的错误捕获,进一步提升代码的可维护性。

9. 总结与拓展

本文围绕 JavaScript 异步编程核心,从同步/异步代码的本质出发,剖析回调地狱的痛点,详细讲解了 Promise 的核心概念、状态管理、三大API(then/catch/finally)及链式调用用法,通过通俗的类比和实战代码,帮助大家快速掌握 Promise 的使用,彻底告别回调地狱。

核心知识点总结:

  • Promise 是 JavaScript 异步编程的核心工具,用于解决异步任务顺序执行、回调嵌套的问题。
  • Promise 有三种状态:pending(等待)、fulfilled(成功)、rejected(失败),状态一旦改变不可修改。
  • resolve 标记成功、传递结果,reject 标记失败、传递原因;then 接收成功结果,catch 接收失败原因,finally 无论成败都执行。
  • 链式调用是 Promise 的核心优势,通过 then 方法返回新的 Promise,实现异步任务的顺序执行,替代回调嵌套。

拓展建议:Promise 是前端异步编程的基础,后续的 async/await 语法(更简洁的异步写法)也是基于 Promise 实现的,掌握本文内容后,可进一步学习 async/await,提升异步编程效率。


学习本就是一场持久战,不需要急着一口吃成胖子。哪怕今天你只记住了一点点,这都是实打实的进步。

后续我还会继续用这种大白话、讲实战方式,带大家扫盲更多前端基础。

关注我,不迷路,咱们把那些曾经模糊的知识点,一个个彻底搞清楚。

如果你觉得这篇内容对你有帮助,不妨点赞+收藏,下次写代码卡壳时,拿出来翻一翻,比搜引擎更靠谱。

我是 Eugene,你的电子学友,我们下一篇干货见~

相关推荐
代码丰2 小时前
简历保险箱:一款本地存储、快捷填表的 Chrome 简历助手
前端·chrome
前端 贾公子2 小时前
uniapp 小程序获取后端的二进制 保存到手机相册
java·前端·javascript
qq_437100662 小时前
echarts图表相关 电量进度图
前端·flask·echarts
智能工业品检测-奇妙智能2 小时前
快速直播:Node.js + FFmpeg + flv.js 全栈实战
javascript·ffmpeg·node.js
Thomas.Sir2 小时前
Vue 3:现代前端框架的架构革命
前端·vue.js·web·大前端
SouthRosefinch2 小时前
三、HTML文本、语义化、列表与表格
前端·html5
清空mega2 小时前
《Vue3 模板语法与常用指令详解:插值、绑定、件、条件渲染、列表渲染一次学会》
前端·javascript·vue.js
周淳APP2 小时前
【HTTP相关及RESTful】风萧萧兮易水寒之壮士学习不复返
前端·javascript·网络·网络协议·http·restful·jsonp
23.2 小时前
缺页中断与页缓存:高效内存管理揭秘
缓存·面试