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 组合为王

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

相关推荐
千百元35 分钟前
jenkins打包问题jar问题
前端
喝拿铁写前端37 分钟前
前端批量校验还能这么写?函数式校验器组合太香了!
前端·javascript·架构
巴巴_羊41 分钟前
6-16阿里前端面试记录
前端·面试·职场和发展
我是若尘43 分钟前
前端遇到接口批量异常导致 Toast 弹窗轰炸该如何处理?
前端
该用户已不存在1 小时前
8个Docker的最佳替代方案,重塑你的开发工作流
前端·后端·docker
然我1 小时前
面试官最爱的 “考试思维”:用闭包秒杀递归难题 🚀
前端·javascript·面试
明月与玄武1 小时前
HTML知识全解析:从入门到精通的前端指南(上)
前端·html
teeeeeeemo2 小时前
CSS place-items: center; 详解与用法
前端·css·笔记
未来之窗软件服务2 小时前
html读取身份证【成都鱼住未来身份证】:CyberWinApp-SAAS 本地化及未来之窗行业应用跨平台架构
前端·html·身份证读取
木木jio2 小时前
🧹 前端日志查询组件的重构实践:从 1600 行巨型组件到模块化 hooks
前端·react.js