Callback 🥊 Promise 🥊 Async/Await:谁才是异步之王?

家人们,我又来了!好久不见,啊啊啊啊啊啊啊啊啊啊啊啊啊,今天来分享异步这个知识点!

一. 引言:为什么需要异步编程?

在编程中,同步(Synchronous)异步(Asynchronous) 是两种不同的代码执行方式,直接影响程序的性能、响应速度和开发模式。

  • 同步(Synchronous) :代码按顺序执行,当前任务未完成时,后续任务必须等待。简单直观,但可能阻塞主线程,资源利用率低,适合简单场景。
  • 异步(Asynchronous) :任务提交后立即继续执行后续逻辑,不等待结果,通过回调、事件或线程池处理结果。可提高资源利用率和程序响应性,适合高并发、I/O 密集型或耗时操作,但代码复杂性较高,需注意线程安全等问题。

在JS中,代码的运行过程是单线程的,那什么是线程呢?为什么是单线程呢?

线程和进程是计算机操作系统中非常重要的概念,它们分别代表了不同的程序执行单元和资源管理方式。

  • 进程:是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位。
  • 线程:是进程中的一个执行单元,是操作系统能够进行运算调度的最小单位。

举个例子来说,就像我们在点开一个浏览器的页面窗口时,就相当于单开了一个进程,而在页面打开时页面的渲染、数据请求等等就相当于是一个个线程。

特别注意的是:JavaScript (JS) 执行和页面渲染在浏览器中是互斥的,不能同时进行。因为 JavaScript 可以修改 DOM 和 CSSOM,从而影响页面的布局和样式,所以当 JavaScript 引擎执行脚本时,它会阻止浏览器进行页面渲染。同样,当浏览器正在进行页面渲染时,JavaScript 的执行会被暂停,直到渲染完成。

因此,JS 引擎在执行 JavaScript 代码时,只有一个线程在运行(除了人为开辟新线程)。如果没有异步处理,那js的执行效率就非常低,任务阻塞会非常严重。所以JS在遇到需要耗时执行的代码就将其先挂起,等到后续不耗时的代码执行完毕后,再回过头来执行耗时的代码,这也就是我们说的异步。

那异步的方案有哪些呢?哪种异步方案更优?

二. Callback:最基础的异步方案

  • 原理:函数作为参数传递,完成后触发回调。
  • 代码示例
JavaScript代码 复制代码
function a(){
    setTimeout(() => {
        console.log('a 执行完毕')
    },1000)
}

function b(){
    setTimeout(() => {
        console.log('b 执行完毕')
    },1500)
}

function c(){
    setTimeout(() => {
        console.log('c 执行完毕')
    },5000)
}
function d(){
    console.log('d 执行完毕')
}

a()
b()
c()
d()

如上面代码所示,因为a、b、c三个函数的内部都是耗时代码,如果不使用回调函数,即不在setTimeout内部代码执行完再执行下一个函数的话,就会导致没有耗时代码的函数d直接执行,1秒后输出"a 执行完毕",1.5秒后输出"b 执行完毕",5秒后输出"c 执行完毕"如图所示: 所以我们想要a、b、c、d的打印按顺序执行,就得使用回调,让每个函数内部的耗时代码执行完后触发回调,下一个函数才执行,代码如下:

JavaScript代码 复制代码
function a(cb,cb2,cb3){
    setTimeout(() => {
        console.log('a 执行完毕')
        cb(cb2,cb3)  //callback b(c, d)
    },1000)
}

function b(cb,cb3){
    setTimeout(() => {
        console.log('b 执行完毕')
        cb(cb3)  //callback c(d)
    },1500)
}

function c(cb){
    setTimeout(() => {
        console.log('c 执行完毕')
        cb()  //callback d()
    },5000)
}
function d(){
    console.log('d 执行完毕')
}

a(b,c,d) 

结果如下: 可以看出回调函数来解决异步问题是非常好理解,也很容易上手的。但是,上面代码示例只是简单的四个函数进行回调嵌套,都看起来有点麻烦。而在实际的开发过程中,可能会出现几十甚至几百个函数相互之间要使用回调,那将会非常麻烦,出现错误的时候也会很难找。总结用回调解决异步的优缺点如下:

  • 优点:简单、兼容性高(所有JS环境支持)

  • 缺点 :会出现回调地狱(Callback Hell),代码嵌套过深,错误处理困难(需手动检查 err),代码难以维护。

三. Promise:拯救回调的"承诺"

1. 什么是 Promise?

Promise 是 JavaScript 中处理异步操作的对象,代表一个未来才会完成的操作 (如网络请求、文件读取等等)。它的原理是通过 状态机(pending/resolved/rejected)+ 链式调用来完成。

2. Promise 的三种状态

  • Pending(进行中) :初始状态,操作未完成。

  • Fulfilled(已成功) :操作成功完成,返回结果值,即resolve()回调函数成功执行,状态转变为Fulfilled,可以在后续操作中获取返回的结果。

  • Rejected(已失败) :操作失败,返回错误原因,有两种情况:

    1. 同步代码中抛出错误(throw

    如果在 Promise 的构造函数或 then/catch 回调同步抛出错误throw),Promise 会自动进入 Rejected 状态,并在后续的 .catch() 中可以捕获该错误。相当于隐式调用了 reject(error)
    2. 异步代码中抛出错误

    如果在 异步回调 (如 setTimeoutfetch.then)中 throwPromise 无法自动捕获 ,会导致全局未处理的错误,需手动 调用reject

值得注意的是:Promise的状态 不可逆(一旦从 Pending 变为 Fulfilled/Rejected,就不能再改变)。

3. 代码示例

JavaScript代码 复制代码
function a(){
    return new Promise(function(resolve,reject){
        // throw new Error("同步错误!"); // 自动触发 reject(error)
        setTimeout(function(){
            console.log('a');
            // resolve('a 执行完毕')  // resolve 调用结束了,.then() 里面的回调函数才会触发
            // reject('a 失败了')  // 人为报错
        },1000)
    })
}

function b(){
    console.log('b');
}

a()
    // 箭头函数的参数只有一个,小括号也可以省略
    .then(res => { //a()的结果是一个 Promise实例对象,所以 .then() 方法是在Promise构造函数的原型上
        console.log(res);
        b()
    })

    .catch((err) => {
        // console.log("捕获到:", err);
        console.log(err); // 捕获错误
    })

如上代码示例,如果resolve不调用,则Promise的状态还是pending,即后续.then()或者.catch()操作无法继续执行,结果如下图:

如果resolve调用,则Promise的状态转换为resolved(),即后续.then()操作执行,结果如下图:

如果在Promise的构造函数的同步代码中抛出错误(throw),就相当于隐式调用了reject,可以直接在catch中获取错误,如下图:

异步代码中,如果reject()调用,则Promise的状态转换为rejected(),即后续.catch()操作执行,结果如下图:

4. then()的返回值

对于then方法,其实也有一套规则决定其返回值的状态:

  • then 方法一定会返回一个新的 Promise对象,状态为pending;
  • then 方法的pending状态会根据上一个 Promise 状态的修改而修改;
  • then 方法可以用自己的 return 覆盖掉then默认返回的promise对象。

这里我们用一段代码举例,来解释then()方法的返回值:

JavaScript代码 复制代码
let count = 0
function A() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            count = 1000
            // resolve() // pending状态 改为 resolved状态
        }, 1000)
    })
}

function B() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            count = 2000
            resolve()
        }, 500);
    })
}

function C() {
    console.log(count);
}

A().then()
    .then(() => {
        C()
    })

// A().then(() => {
//     B().then(() => {
//         C()
//     })
// })

// 优化后
// A().then(() => {
//     return B()
// })
//     .then(() => {
//         C()
//     })

首先,第24行代码then的后面还能够接then,说明then天生返回一个promise对象,才能接.then。

其次,我们让A返回的Promise状态还是pending,直接运行代码,会发现A运行后不再执行后续then,结果如下: 接着,如果我们将A的Promise对象状态改为resolved,第二个then也执行了,所以能够执行从c()的调用。这就说明第一个then返回的Promise状态由pending改为了resolved 了,才能继续走第二个then。如图: 如果我想要函数A调用后,执行函数B之后再执行函数C,就会想到在then里面调用B,然后再接then,如代码29-33行,这样如果函数多了起来,就会显得很麻烦。

于是,我们就会用到第三个规则,return重新返回一个Promise对象,如代码35-41行。利用then后面还可以接then的方式,我们在第一个then的最后返回一个状态为resolved的promise对象B,这样接在后面的then可以直接执行,一下就清晰了代码的运行顺序,更加便捷不易出错。

5. Promise的优缺点

  • 优点

    • 链式调用解决回调嵌套
    • 统一的错误处理(.catch
    • 支持 Promise.all/Promise.race 等高级操作
  • 缺点

    • 仍需处理 .then
    • 无法取消(除非手动封装)

四. Async/Await:同步写法的异步魔法

async声明一个函数是异步函数,写在函数声明之前,总是返回一个Promiseawait只能在 async 函数中使用,暂停代码执行,等待 Promise 解决。

  • 原理:Generator + Promise 的语法糖
  • 代码示例
JavaScript代码 复制代码
// async await
function a() {
    return new Promise(function(resolve,reject){
        setTimeout(function () {
            console.log("a");
            resolve()
        }, 1000)
    }
    )
}

function b(){
    console.log('b');
}

async function fn(){
    // await a() // await 只能操作promise对象,且promise执行成功了才能执行后面的
    // b()
}

console.log(fn()); //Promise { undefined } async 相当于是new Promise

如代码所示,我们在使用async声明fn函数,直接返回fn()的调用结果,能够得到一个空的Promise对象,如图:

我们结合await,需要在await后面接一个Promise,即我们接函数a,如上代码17-18行,运行结果如图: 为什么一开始打印的是pending状态的Promise呢?

因为await后面接的Promise会覆盖async的函数的返回值,就是函数a。在同步代码a()执行时,a返回的Promise状态还没有变为resolved状态,所以返回的是一个pending状态的Promise。

其次,await会等待后面接的Promise对象完全执行完成后,才会执行后面的代码b()。

  • 优点

    • 代码像同步一样直观
    • 错误处理更自然(try/catch
    • 调试友好(可逐行执行)
  • 缺点

    • 需在 async 函数中使用
    • 底层仍是 Promise,理解成本略高

五. 终极对比:谁才是"异步之王"?

维度 Callback Promise Async/Await
可读性 ❌ 差 ✅ 较好 ✅✅ 最佳
错误处理 ❌ 手动检查 .catch try/catch
调试难度 ❌ 困难 ⚠️ 中等 ✅ 容易
兼容性 ✅ 所有环境 ✅ ES6+ ✅ ES2017+
适用场景 传统库兼容 复杂异步逻辑 现代项目开发

六. 实战建议:如何选择?

  • 用 Callback:维护旧代码或底层库开发(如 Node.js 核心模块)
  • 用 Promise :需要并行处理多个异步任务(如 Promise.all
  • 用 Async/Await:绝大多数现代项目,尤其是逻辑复杂的场景

七. 结语:没有银弹,只有最合适的工具

  • 技术选型需权衡项目需求、团队习惯和运行环境
  • 未来趋势:Async/Await + Promise 组合为王

这期的分享就到这里啊哈哈哈,下次见,喜欢的话,记得点个赞点个关注喔~

相关推荐
gnip19 分钟前
链式调用和延迟执行
前端·javascript
SoaringHeart30 分钟前
Flutter组件封装:页面点击事件拦截
前端·flutter
杨天天.32 分钟前
小程序原生实现音频播放器,下一首上一首切换,拖动进度条等功能
前端·javascript·小程序·音视频
Dragon Wu42 分钟前
React state在setInterval里未获取最新值的问题
前端·javascript·react.js·前端框架
Jinuss43 分钟前
Vue3源码reactivity响应式篇之watch实现
前端·vue3
YU大宗师1 小时前
React面试题
前端·javascript·react.js
木兮xg1 小时前
react基础篇
前端·react.js·前端框架
ssshooter1 小时前
你知道怎么用 pnpm 临时给某个库打补丁吗?
前端·面试·npm
IT利刃出鞘2 小时前
HTML--最简的二级菜单页面
前端·html
yume_sibai2 小时前
HTML HTML基础(4)
前端·html