作为前端开发,异步编程是绕不开的坎——请求接口、定时器、文件上传,几乎所有业务场景都离不开它。
很多新手被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必须掌握的细节
- await只能用在async函数内部,普通函数中使用会报错;
- 未用try/catch包裹await,异步操作失败时,会报未捕获错误;
- 不要滥用await:不需要顺序执行的异步,不要逐个await,否则会降低效率(用Promise.all并行);
- 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并行请求多个接口,练多了自然就能熟练掌握。
很多人觉得异步难,只是因为没找对方法:不用纠结复杂的原理,先上手写,遇到错误再对照排查,慢慢就会发现,异步编程其实很简单。
各位互联网搭子,要是这篇文章成功引起了你的注意,别犹豫,关注、点赞、评论、分享走一波,让我们把这份默契延续下去,一起在知识的海洋里乘风破浪!