吃透JS异步编程:从Promise到async/await,新手也能轻松上手

0 阅读7分钟

作为前端开发,异步编程是绕不开的坎——请求接口、定时器、文件上传,几乎所有业务场景都离不开它。

很多新手被Promise、async/await、回调地狱搞得头晕,要么写出来的代码混乱不堪,要么频繁出现异步执行顺序错误,排查起来费时又费力。

今天这篇文章,不搞复杂理论,只讲实战落地的JS异步编程技巧,从基础回调到Promise,再到async/await,一步步拆解,配合可直接复制的代码示例,新手也能快速吃透,彻底摆脱异步困扰。


一、先搞懂:为什么需要异步编程?

JS是单线程语言,一次只能执行一个任务。如果所有操作都是同步的,比如接口请求需要3秒,页面会卡住3秒,用户体验极差。

异步编程的核心的是:不阻塞主线程,让耗时操作在后台执行,执行完成后再通知主线程处理结果

常见的异步场景:

  • 接口请求(axios/fetch)
  • 定时器(setTimeout/setInterval)
  • 文件读取/上传
  • 事件监听(click/load)

二、回调函数:异步编程的基础(避坑重点)

回调函数是最基础的异步实现方式,简单说就是“把一个函数作为参数,传给另一个函数,异步操作完成后执行这个函数”。

1. 基础用法(定时器示例)

// 回调函数:异步操作完成后执行
setTimeout(() => {
  console.log('异步操作完成');
}, 1000);

console.log('主线程任务'); 
// 输出顺序:主线程任务 → 异步操作完成(1秒后)

2. 避坑点:回调地狱(千万不要这么写)

当多个异步操作嵌套时,会出现“回调套回调”的情况,代码可读性极差,难以维护,这就是回调地狱。

// 错误示例:回调地狱(嵌套层级越多,越难维护)
setTimeout(() => {
  console.log('第一步');
  setTimeout(() => {
    console.log('第二步');
    setTimeout(() => {
      console.log('第三步');
    }, 1000);
  }, 1000);
}, 1000);

3. 回调地狱解决方案:拆分函数(临时过渡)

虽然不能彻底解决,但可以通过拆分函数,提升代码可读性,为后续用Promise铺垫。

// 正确做法:拆分函数
function step1(callback) {
  setTimeout(() => {
    console.log('第一步');
    callback(); // 执行下一个步骤
  }, 1000);
}

function step2(callback) {
  setTimeout(() => {
    console.log('第二步');
    callback();
  }, 1000);
}

function step3() {
  setTimeout(() => {
    console.log('第三步');
  }, 1000);
}

// 调用:链式执行,避免嵌套
step1(() => {
  step2(() => {
    step3();
  });
});

三、Promise:彻底解决回调地狱(核心重点)

Promise是ES6引入的异步编程解决方案,它把异步操作封装成一个“容器”,用链式调用替代嵌套,代码更简洁、易维护。

1. Promise基础语法(3个状态)

Promise有三个状态,一旦状态改变,就不会再变:

  • pending:等待中(初始状态)
  • fulfilled:成功(异步操作完成)
  • rejected:失败(异步操作出错)
// 基础语法:创建Promise实例
const promise = new Promise((resolve, reject) => {
  // 异步操作(比如接口请求)
  setTimeout(() => {
    const success = true;
    if (success) {
      // 成功:调用resolve,传递结果
      resolve('异步操作成功');
    } else {
      // 失败:调用reject,传递错误信息
      reject(new Error('异步操作失败'));
    }
  }, 1000);
});

// 调用Promise:链式调用(then成功,catch失败)
promise
  .then((res) => {
    console.log(res); // 异步操作成功
  })
  .catch((err) => {
    console.log(err.message); // 异步操作失败
  })
  .finally(() => {
    console.log('无论成功失败,都会执行'); // 收尾操作(可选)
  });

2. 用Promise解决回调地狱(推荐写法)

把每个异步操作封装成Promise,用then链式调用,彻底摆脱嵌套。

// 封装每个步骤为Promise
function step1() {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log('第一步');
      resolve();
    }, 1000);
  });
}

function step2() {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log('第二步');
      resolve();
    }, 1000);
  });
}

function step3() {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log('第三步');
      resolve();
    }, 1000);
  });
}

// 链式调用:顺序执行,无嵌套
step1()
  .then(() => step2())
  .then(() => step3())
  .catch((err) => console.log(err));

3. 高频实用:Promise.all(并行执行多个异步)

如果多个异步操作互不依赖,不需要顺序执行,用Promise.all可以并行执行,提升效率(所有操作都成功才返回成功)。

// 并行执行3个异步操作(接口请求常用)
const promise1 = new Promise((resolve) => setTimeout(() => resolve(1), 1000));
const promise2 = new Promise((resolve) => setTimeout(() => resolve(2), 2000));
const promise3 = new Promise((resolve) => setTimeout(() => resolve(3), 1500));

// 并行执行,所有成功后返回结果数组(顺序和传入一致)
Promise.all([promise1, promise2, promise3])
  .then((res) => {
    console.log(res); // [1, 2, 3](2秒后返回,取最长的异步时间)
  })
  .catch((err) => {
    // 只要有一个失败,就执行catch
    console.log(err);
  });

4. 避坑点:Promise常见错误

  • 忘记写catch:异步操作失败时,会报未捕获错误,导致程序卡死;
  • 直接调用Promise,忘记then:Promise不会自动执行,必须调用then/catch才会触发;
  • 嵌套使用Promise:虽然比回调地狱好,但仍不推荐,尽量用链式调用。

四、async/await:Promise的语法糖(最简洁写法)

async/await是ES7引入的,基于Promise,让异步代码看起来和同步代码一样,可读性拉满,是目前最推荐的异步编程方式。

1. 基础语法(核心关键字)

  • async:修饰函数,表明这个函数是异步函数,返回值自动包装成Promise;
  • await:只能用在async函数内部,等待Promise完成,拿到结果后再继续执行。
// 基础用法:async + await
async function fetchData() {
  try {
    // 等待Promise完成,拿到结果(避免then链式调用)
    const res = await new Promise((resolve) => {
      setTimeout(() => resolve('接口返回数据'), 1000);
    });
    console.log(res); // 接口返回数据
  } catch (err) {
    // 捕获异步操作失败的错误(对应Promise的catch)
    console.log(err.message);
  }
}

// 调用异步函数
fetchData();

2. 顺序执行多个异步(替代Promise链式)

如果需要顺序执行多个异步操作,async/await比Promise链式更简洁,逻辑更清晰。

async function executeStep() {
  try {
    await step1(); // 等待第一步完成
    await step2(); // 第一步完成后,执行第二步
    await step3(); // 第二步完成后,执行第三步
  } catch (err) {
    console.log('某个步骤失败:', err);
  }
}

executeStep();

3. 并行执行多个异步(配合Promise.all)

async/await也能实现并行执行,只需把多个Promise放在数组中,用await配合Promise.all即可。

async function fetchAllData() {
  try {
    // 并行执行,等待所有异步完成
    const [res1, res2, res3] = await Promise.all([
      promise1,
      promise2,
      promise3
    ]);
    console.log(res1, res2, res3); // 1 2 3
  } catch (err) {
    console.log(err);
  }
}

fetchAllData();

4. 避坑点:async/await必须掌握的细节

  1. await只能用在async函数内部,普通函数中使用会报错;
  2. 未用try/catch包裹await,异步操作失败时,会报未捕获错误;
  3. 不要滥用await:不需要顺序执行的异步,不要逐个await,否则会降低效率(用Promise.all并行);
  4. async函数返回值:无论return什么,都会自动包装成Promise,比如return 1 → Promise.resolve(1)。

五、实战场景:接口请求异步处理(真实项目常用)

结合axios(接口请求库),用async/await实现接口请求,这是真实项目中最常用的写法,简洁、易维护。

// 1. 安装axios
// npm install axios

// 2. 封装接口请求(utils/request.js)
import axios from 'axios';

// 创建axios实例
const request = axios.create({
  baseURL: import.meta.env.VITE_API_URL,
  timeout: 10000
});

// 3. 异步请求函数(用async/await)
export async function getUserList(params) {
  try {
    const res = await request({
      url: '/user/list',
      method: 'get',
      params
    });
    return res.data; // 返回接口数据
  } catch (err) {
    // 统一错误处理
    console.log('获取用户列表失败:', err);
    throw err; // 抛出错误,让调用者处理
  }
}

// 4. 组件中使用
async function loadUserList() {
  const params = { page: 1, size: 10 };
  try {
    const data = await getUserList(params);
    console.log('用户列表:', data);
    // 渲染页面数据
  } catch (err) {
    // 页面错误提示
    ElMessage.error('加载失败,请重试');
  }
}

// 调用
loadUserList();

写在最后

JS异步编程的学习路径,其实很简单:回调函数(基础)→ Promise(核心)→ async/await(推荐)

新手不用一开始就死记硬背理论,先记住核心用法,多写实战代码——比如用async/await封装一个接口请求,用Promise.all并行请求多个接口,练多了自然就能熟练掌握。

很多人觉得异步难,只是因为没找对方法:不用纠结复杂的原理,先上手写,遇到错误再对照排查,慢慢就会发现,异步编程其实很简单。


各位互联网搭子,要是这篇文章成功引起了你的注意,别犹豫,关注、点赞、评论、分享走一波,让我们把这份默契延续下去,一起在知识的海洋里乘风破浪!