一、什么是异步编程

(1)、同步和异步的区别

以下用一个例子来说明同步和异步的区别

传统同步模式(低效阻塞) 流程:顾客进店,服务员全程跟进 —— 帮点餐→送后厨→站在厨房门口等菜做好→端菜给顾客,全程无法接待其他顾客。

问题:若菜品烹饪需 10 分钟,服务员被完全卡住,后面的顾客只能排队干等,餐厅效率极低。

现代异步模式(高效非阻塞) 流程:顾客进店,服务员递扫码点餐码(发起异步任务)后,立刻去接待下一位顾客(执行其他任务);后厨收到订单后独立做菜(处理耗时操作),菜做好后按铃通知(异步任务完成触发回调);服务员听到铃声,再回来给该顾客端菜(处理回调)。

(2)、为什么会有异步编程

在JS中代码是单线程编译的(单线程指的是程序在执行过程中,只有一个执行流(线程) 来依次处理任务,同一时间只能执行一个指令,所有任务都在这个唯一的线程中按顺序排队执行)也就是同步。

很多人会疑惑:JavaScript 是单线程的,但为什么能处理异步操作(如网络请求、定时器)?这并非单线程本身支持并行,而是借助了浏览器 提供的多线程辅助模块和事件循环(Event Loop) 机制,(后面会涉及)。

异步编程是一种编程范式,核心是解决程序中耗时操作(如网络请求、文件读写、数据库查询等)导致的阻塞问题,让程序在等待耗时操作完成的过程中,不闲置而是继续执行其他任务,待耗时操作完成后再回调处理其结果。

二、异步编程的实现方法

回调函数,Promise、async/await、Ajax、

(1)回调函数(最基础)
// 模拟三个依赖的异步任务
function task1(callback) {
  setTimeout(() => {
    console.log("任务1完成");
    callback("任务1的结果");
  }, 1000);
}

function task2(data, callback) {
  setTimeout(() => {
    console.log("任务2完成,依赖:", data);
    callback("任务2的结果");
  }, 1000);
}

function task3(data, callback) {
  setTimeout(() => {
    console.log("任务3完成,依赖:", data);
    callback("任务3的结果");
  }, 1000);
}

// 嵌套调用:回调地狱
task1((res1) => {
  task2(res1, (res2) => {
    task3(res2, (res3) => {
      console.log("所有任务完成,最终结果:", res3);
    });
  });
});

回调函数是解决异步编程最基础的方法,在调用少的情况下缺陷不明显,但是在回调多的时候就会有很明显的可读性和维护性方法的缺陷——回调地狱。

以上引入了异步编程的基础实现方式,来理解异步的执行逻辑,下面来进行当前异步编程的核心实现方法和编程逻辑。

(2)Promise
  • Promise是ES6正式纳入标准来解决回调地狱的的基础语法。

  1. Promise通过链式调用使代码的可读性更强,流程更清晰

promise
  .then((result) => {
    console.log("成功:", result); // 输出:成功:异步操作成功的结果
  })
  .catch((error) => {
    console.log("失败:", error);
  })
  .finally(() => {
    console.log("异步操作结束(无论成败)");
  });

b.Promise的错误处理更清晰

Promise的错误具有冒泡的特性,链式调用中的任何一个环节抛出错误都会传递到最近的catch中,所以一般都会在链式调用的末尾统一捕获错误。

function task1() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      const isError = false; // 模拟失败:改为 true
      if (isError) {
        reject("任务1执行失败"); // 触发失败
      } else {
        console.log("任务1完成");
        resolve("任务1的结果");
      }
    }, 1000);
  });
}

// 链式调用 + 统一错误捕获
task1()
  .then((res1) => task2(res1))
  .then((res2) => task3(res2))
  .then((res3) => console.log("最终结果:", res3))
  .catch((error) => console.log("错误:", error));
  1. Promise的静态方法

1、Promise.all

等待所有的Promise都成功之后返回成功结果,若有一个失败则立即返回失败原因

2、Promise.race

等待第一个状态变更的Promise

const request = new Promise((resolve) => {
  setTimeout(() => resolve("接口请求成功"), 3000);
});
const timeout = new Promise((_, reject) => {
  setTimeout(() => reject("请求超时(5秒)"), 5000);
});

Promise.race([request, timeout])
  .then((res) => console.log(res)) // 输出:接口请求成功
  .catch((err) => console.log(err));

3、Promise.resolve

立即创建一个成功的Promise

4、Promise.reject

立即创建一个失败的Promise

5、Promise.allSettled

等待所有的Promise完成(无论是否成功),返回每个Promise的结果对象

const p1 = Promise.resolve(1);
const p2 = Promise.reject("失败了");

Promise.allSettled([p1, p2])
  .then((results) => console.log(results));
// 输出:
// [
//   { status: 'fulfilled', value: 1 },
//   { status: 'rejected', reason: '失败了' }
// ]

6、Promise.any

等待第一个完成的Promise,若都失败则返回一个AggregateError(包含所有失败原因)。

7、.then

处理成功/失败结果

8、.catch

捕获错误,用来处理失败结果

9、.finally

无论Promise是否成功都会执行,常用于清理操作。

(3)async/await(重点掌握)

1、为什么会出现async和await——promise的不足

Promise 解决了回调地狱的嵌套问题,通过链式调用将横向嵌套转为纵向流程,但仍有不足:

  • 链式调用过长时,依然会出现「.then 链」,逻辑连贯性不如同步代码;

  • 复杂的异步流程(如条件分支、循环)在 Promise 中实现时,代码依然繁琐;

  • 异常处理虽有 catch,但多链并行时的异常捕获仍需额外处理。

// Promise 链式调用:仍有链式冗余
fs.promises.readFile('file1.txt')
  .then(data1 => {
    return fs.promises.readFile('file2.txt').then(data2 => [data1, data2]);
  })
  .then(([data1, data2]) => {
    return fs.promises.readFile('file3.txt').then(data3 => [data1, data2, data3]);
  })
  .then(([data1, data2, data3]) => {
    console.log(data1 + data2 + data3);
  })
  .catch(err => console.error(err));

代码仍需围绕 then 展开,无法像同步代码一样「线性书写」。

async/await 是 Promise 的语法糖,它的诞生就是为了彻底解决上述问题,核心目标是:用同步代码的书写方式实现异步逻辑,同时保留 Promise 的异步特性和异常处理能力。

async/await 让异步操作的代码不再依赖嵌套或链式调用,而是像同步代码一样按顺序书写,可读性和可维护性大幅提升。

// async/await 线性书写:和同步代码逻辑一致
async function readFiles() {
  try {
    const data1 = await fs.promises.readFile('file1.txt');
    const data2 = await fs.promises.readFile('file2.txt');
    const data3 = await fs.promises.readFile('file3.txt');
    console.log(data1 + data2 + data3);
  } catch (err) {
    console.error(err);
  }
}

await关键字只能在async内使用,它会暂停函数的执行,直到Promise状态变为fulfilled再继续执行并返回结果。async和await与Primise的静态方法结合,可以实现错误捕获的更便利的方法

// 模拟成功的请求:获取用户信息
function getUserInfo() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ id: 1, name: "张三", age: 20 });
    }, 1000);
  });
}

// 模拟失败的请求:获取用户订单(模拟接口报错)
function getUserOrder() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject(new Error("获取订单失败:接口超时")); // 模拟错误
    }, 1000);
  });
}

async function fetchSingleData() {
  try {
    // 尝试请求失败的接口
    const order = await getUserOrder();
    console.log("用户订单:", order); // 失败时不会执行
  } catch (error) {
    // 捕获错误并处理
    console.error("单请求错误:", error.message);
  } finally {
    // 可选:无论成败都会执行,用于清理操作(如关闭加载)
    console.log("单请求执行完成(无论成败)\n");
  }
}

// 执行函数
fetchSingleData();

// async/await + try/catch(推荐,代码更同步)
async function fetchAllData() {
  try {
    // 并行请求:一个成功,一个失败
    const [userInfo, userOrder] = await Promise.all([
      getUserInfo(),
      getUserOrder(),
    ]);
    console.log("并行请求结果:", { userInfo, userOrder }); // 失败时不会执行
  } catch (error) {
    console.error("Promise.all 错误:", error.message);
  }
}

//不使用.all方法,代码会更多
async function fetchAllData() {
  try {
    // 1. 手动启动所有异步任务(并行执行,立即调用函数)
    const userInfoPromise = getUserInfo();
    const userOrderPromise = getUserOrder();

    // 2. 手动等待每个 Promise 完成(效果等价于 Promise.all)
    // 注意:这里依然是并行,因为两个 Promise 已经提前启动
    const userInfo = await userInfoPromise;
    const userOrder = await userOrderPromise;

    console.log("并行请求结果:", { userInfo, userOrder });
  } catch (error) {
    console.error("手动并行错误:", error.message);
  }
}

fetchAllData();

三、异步编程的核心机制:事件循环

定义

事件循环(Event Loop)是JavaScript 实现异步编程的核心机制,因为 JS 是单线程语言,无法同时执行多任务,事件循环通过调度任务的执行顺序,让 JS 能够处理异步操作(如网络请求、定时器、DOM 事件),避免单线程阻塞。

事件循环的核心概念
  1. 执行栈(Call Stack)

    • 单线程的「任务执行区」,遵循后进先出(LIFO) 原则。

    • 所有同步任务都会被依次压入执行栈中,执行完一个弹出一个,直到栈为空。

    • 示例:调用函数时,函数会被压入栈;函数执行完毕,从栈中弹出。

  2. 任务队列(Task Queue)

    • 存放异步任务的回调函数的容器,遵循先进先出(FIFO) 原则。

    • 异步任务完成后(如定时器到时间、网络请求返回),其回调会被加入任务队列,等待执行。

  3. 异步任务的分类

    事件循环中的异步任务被分为两类,执行优先级不同,这是理解事件循环的关键:

    • 宏任务(Macrotask):粒度较大的异步任务,每次事件循环仅执行一个宏任务。

      常见类型:

    • 微任务(Microtask):粒度较小的异步任务,每次宏任务执行完后,会清空所有微任务队列中的任务。

核心逻辑

可以总结为「先同步、再微任务、再宏任务」(微任务的优先级大于宏任务)

案例
console.log('同步代码1'); // 同步任务

setTimeout(() => { // 宏任务
  console.log('setTimeout 宏任务');
}, 0);

Promise.resolve().then(() => { // 微任务
  console.log('Promise 微任务1');
  return '微任务返回值';
}).then(res => { // 微任务执行中产生的新微任务
  console.log('Promise 微任务2', res);
});

console.log('同步代码2'); // 同步任务

答案

同步代码1
同步代码2
Promise 微任务1
Promise 微任务2 微任务返回值
setTimeout 宏任务
console.log(1)
new Promise((resolve, reject) => {
    console.log(2)
    resolve()
}).then(res => {
    console.log(3)
})
console.log(4)
输出
1,2,4,3

四、异步在浏览器端的使用场景

异步在浏览器的使用场景最常用的为:

1、网络请求

最常见的场景,如通过fetch/axios请求后端接口、加载图片 / 视频 / 脚本等静态资源,必须等待服务端响应或资源加载完成才能处理数据;

2、DOM事件监听

用户交互是异步触发的,如点击 (click)、滚动 (scroll)、输入 (input) 等事件,程序需要异步监听并处理;

3、本地存储操作

读写localStorage/sessionStorage(虽同步 API 为主,但 IndexedDB 为异步)、文件上传 / 下载(通过FileReader读取本地文件);

4、Web API操作(了解)

Geolocation获取地理位置、Web Worker处理后台计算、WebSocket实时通信。

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐