今天我们来聊一聊JavaScript中最重要也最难掌握的概念——异步编程。
这是现代前端开发的基石,也是面试中必问的高频考点。
系列文章目录
解密JavaScript面向对象(一):从新手到高手,手写call/bind实战
解密JavaScript面向对象(二):深入原型链,彻底搞懂面向对象精髓
解密作用域与闭包:从变量访问到闭包实战一网打尽
文章目录
一、从同步到异步:为什么需要异步编程?
同步编程的困境
想象一下,你在餐厅点餐:
// 同步编程:排队点餐,一个个来
function synchronousRestaurant() {
console.log("开始点餐");
const order1 = makeOrder("鱼香肉丝"); // 等待10分钟
console.log("订单1完成:", order1);
const order2 = makeOrder("宫保鸡丁"); // 再等10分钟
console.log("订单2完成:", order2);
const order3 = makeOrder("麻婆豆腐"); // 又等10分钟
console.log("订单3完成:", order3);
console.log("所有订单完成");
}
// 模拟做菜函数(同步阻塞)
function makeOrder(dish) {
const start = Date.now();
// 模拟耗时操作(阻塞10秒)
while (Date.now() - start < 10000) {}
return `${dish}做好了`;
}
问题:如果每个菜要做10分钟,3个菜就要30分钟🤫。后面的顾客要饿死了❗️
异步编程的解决方案
// 异步编程:点完餐就可以做其他事
function asynchronousRestaurant() {
console.log("开始点餐");
// 异步下单,不等待立即返回
makeOrderAsync("鱼香肉丝", (order) => {
console.log("订单1完成:", order);
});
makeOrderAsync("宫保鸡丁", (order) => {
console.log("订单2完成:", order);
});
makeOrderAsync("麻婆豆腐", (order) => {
console.log("订单3完成:", order);
});
console.log("已提交所有订单,可以去喝茶了");
}
// 异步做菜函数
function makeOrderAsync(dish, callback) {
setTimeout(() => {
callback(`${dish}做好了`);
}, 10000); // 10秒后回调
}
优势:非阻塞,提高效率,更好的用户体验!
二、异步编程演进史:技术的迭代变迁
在单线程的 JavaScript 环境中,在不阻塞主线程的情况下,去处理耗时的任务,当耗时任务处理完成通过某种机制来通知主线程处理操作结果,这就是异步编程。
这个机制就是异步编程要解决的核心问题,而回调函数就是最初解决这个困境的“通知机制”。
第一代:回调函数(Callback)
// 解决
function getUserData(userId, callback) {
// *** 处理逻辑doSomething
const result = doSomething();
// 通知主线程处理结果
callback(result);
}
// 带来的新问题 - 回调地狱
function getUserData(userId, callback) {
getUserInfo(userId, (userInfo) => {
getuserOrders(userInfo.orderId, (orders) => {
getOrderDetails(orders[0].id, (details) => {
calculateTotal(details, (total) => {
callback(total);
});
});
});
});
}
新问题:回调地狱,难以维护和处理错误
第二代:Promise对象
Promise在异步演进中的承上启下地位,它用标准化管道消除了回调的混乱。它通过标准的 .then() 和 .catch() 方法,将嵌套的回调函数变成了链式的、顺序的写法,简洁且有了更好的错误处理方案。
then()方法接收处理成功的情况,catch()接收失败的情况。
function getUserData(userId) {
return getUserInfo(userId)
.then(userInfo => getuserOrders(userInfo.orderId))
.then(orders => getOrderDetails(orders[0].id))
.then(details => calculateTotal(details));
}
第三代:Async/Await语法糖
Async/Await是基于Promise的封装。
Await 把基于 .then() 和 .catch() 的“链式调用”进行了包装,让处理异步任务的代码,在书写和阅读上,变得和传统的同步代码几乎一模一样,是异步编程发展史上的一个里程碑。
async function getUserData(userId) {
try {
const userInfo = await getUserInfo(userId);
const orders = await getuserOrders(userInfo.orderId);
const details = await getOrderDetails(orders[0].id);
const total = await calculateTotal(details);
return total;
} catch (error) {
console.error("获取数据失败:", error);
}
}
革命性进步:代码像同步一样清晰,错误处理简单!
下一代 - 不再展开
1)Top-level Await (ES2022):允许再模块的顶层作用域直接使用 await,无需包裹在 async 函数中。
2)Promise.withResolvers() (ES2024):返回一个对象,包含 promise、resolve 和 reject 三个属性,避免了在外部声明 resolve 和 reject 变量的冗余
3)提案中:
Async Context :提供一种在异步调用链中跟踪和传递上下文信息的机制
Promise.try:一种静态方法,接收函数并返回Promise。用于统一同步错误和异步错误的捕获路径。
三、Promise原理及实现
Promise 的本质是一个“异步任务的状态契约”。
它包含三种明确的状态:1)pending:进行中;2)fulfilled:已成功;3)rejected:已失败
下方为一个Promise调用实例
// 基本用法
const promise = new Promise((resolve, reject) => {
// 异步操作
setTimeout(() => {
const random = Math.random();
if (random > 0.5) {
resolve(`成功: ${random}`);
} else {
reject(`失败: ${random}`);
}
}, 1000);
});
promise.then(result => console.log(result))
.catch(error => console.error(error))
.finally(() => console.log("执行完毕"));
这要说明了Promise的四个特点:
1)执行了resolve,Promise状态会变成fulfilled;
2) 执行了reject,Promise状态会变成rejected;
3)Promise状态不可逆,第一次成功就永久为fulfilled,第一次失败就永远状态为rejected;
4) Promise中有throw的话,就相当于执行了reject;
3.1 手写Promise之实现resolve和reject
实现思路:
1)关于参数
1.1)设置内部状态变量,初始状态为等待中;
1.2)设置两个值存储变量:一个存储成功结果值,一个存储失败原因
2) 关于resolve/reject 函数
2.1 )检查当前状态是否为等待中,只有等待中状态才能转换
2.2)更改状态变量:从等待中变更为已成功/已失败
2.3)存储成功/失败的结果值
3)在构造函数中立即执行用户传入的执行器函数,将内部的 resolve 和 reject 函数作为参数传递给执行器,执行函数
class MyPromise01 {
constructor(executor) {
this.state = 'pending'; // 状态:pending, fulfilled, rejected
this.value = undefined; // 成功值
this.reason = undefined; // 失败原因
const resolve = (value) => {
if (this.state === 'pending') {
console.log("resolved");
this.state = 'fulfilled';
this.value = value;
}
};
const reject = (reason) => {
if (this.state === 'pending') {
console.log("rejected");
this.state = 'rejected';
this.reason = reason;
}
};
try {
// 执行传进来的函数
executor(resolve, reject);
} catch (error) {
reject(error);
}
}
}
// 测试手动实现的MyPromise01
const p = new MyPromise01((resolve) => {
setTimeout(() => resolve('成功'), 1000);
});
3.2 手写Promise之实现then方法
实现思路:
1)接收两个回调,一个是成功回调,一个是失败回调;
2)当Promise状态为fulfilled执行成功回调,为rejected执行失败回调;
3)若Promise 仍在等待中(如resolve或reject在定时器里),则存储回调函数供后续状态改变时(定时器结束)使用;
4)then方法返回新的 Promise
4)实现链式调用:即then方法后,还行执行then方法,则需要保证then执行后的值包装成一个Promise对象返回。
ps: 需要在构造函数中添加回调数组,保存回调函数
class MyPromise {
// 构造方法
constructor(executor) {
// 初始化参数
this.state = 'pending'; // 状态:pending, fulfilled, rejected
this.value = undefined; // 成功值
this.reason = undefined; // 失败原因
// 增加回调保存,便于状态变量更改后执行
this.onFulfilledCallbacks = [] // 保存成功回调
this.onRejectedCallbacks = [] // 保存失败回调
// resolve函数
executor(resolve, reject)
} catch (e) {
// 捕捉到错误直接执行reject
reject(e)
}
}
// then方法
then(onFulfilled, onRejected) {
// 接收两个回调 onFulfilled, onRejected
// 参数校验,确保一定是函数
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value
onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason }
const thenPromise = new MyPromise((resolve, reject) => {
if (this.state === 'fulfilled') {
// 使用 setTimeout 的核心原因是为了模拟原生 Promise 的异步执行特性;
// 若不使用,回调会同步立即执行,这违反了 Promise 规范
setTimeout(() => {
// 使用try-catch包裹,使用catch捕获onFulfilled或onRejected执行中的异常
try{
// 如果当前为成功状态,执行第一个回调
const returnX = onFulfilled(this.value);
// 将返回值封装,便于链接调用
this.afterThenPromise(thenPromise, returnX, resolve, reject);
}catch(err){
reject(err);
}
});
} else if (this.state === 'rejected') {
setTimeout(() => {
try{
// 如果当前为失败状态,执行第二个回调
const returnX = onRejected(this.reason);
this.afterThenPromise(thenPromise, returnX, resolve, reject);
}catch(err){
reject(error);
}
})
} else if (this.state === 'pending') {
// 如果状态为待定状态,暂时保存两个回调
this.onFulfilledCallbacks.push(() => {
setTimeout(() => {
try {
const returnX = onFulfilled(this.value);
this.afterThenPromise(thenPromise, returnX, resolve, reject);
} catch (error) {
reject(error);
}
});
});
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
try {
const returnX = onRejected(this.reason);
this.afterThenPromise(thenPromise, returnX, resolve, reject);
} catch (error) {
reject(error);
}
});
});
}
});
// 返回这个包装的Promise
return thenPromise;
}
// then方法执行后将返回值封装可执行Promise
afterThenPromise(thenPromise, returnX, resolve, reject){
// 判定返回是否为自身,防止循环引用
if (returnX === thenPromise) {
return reject(new TypeError('Chaining cycle detected'));
}
// 如果returnX是Promise,等待其完成
if (returnX instanceof MyPromise02) {
returnX.then(resolve, reject);
} else {
// 结果非Promise就直接成功
resolve(returnX);
}
}
}
// 测试使用
const p = new MyPromise((resolve) => {
setTimeout(() => resolve(100), 1000);
});
p.then(result => {
console.log(result); // 1秒后输出"100"
return 2*result;
}).then(result => {
console.log(result); // 输出"200"
});
PS: 此示例中便于理解存在冗余代码,尝试过程中可自行优化。
3.3 手写Promise之关于其他方法
3.3.1 all:
1) 接收一个Promise数组,数组中如有非Promise项,则此项当做成功;
2)若都成功,返回成功结果数组;存在失败,则返回第一个失败结果
static all(promises) {
const result = [];
let count = 0;
return new MyPromise((resolve, reject) => {
// 保存成功结果,并计算结果数量
const addData = (index, value) => {
result[index] = value;
count++;
if (count === promises.length) resolve(result)
}
// 循环处理promise函数
promises.forEach((promise, index) => {
if (promise instanceof MyPromise) {
promise.then(res => {
addData(index, res)
}, err => reject(err))
} else {
// 非promise项,当作成功
addData(index, promise)
}
})
})
}
3.3.2 race
1)接收一个Promise数组,数组中如有非Promise项,则此项当做成功;
2)哪个Promise最快得到结果,就返回那个结果,无论成功失败;
3.3.3 allSettled
1)接收一个Promise数组,数组中如有非Promise项,则此项当做成功;
2)把每一个Promise的结果,集合成数组后返回;
3.3.4 any
与all相反
1)接收一个Promise数组,数组中如有非Promise项,则此项当做成功;
2)若有一个Promise成功,则返回这个成功结果;若所有Promise都失败,则报错。
race/allSettled/any函数的实现,有兴趣自行实现,有问题可以在评论区留言
四、Async/Await原理与实现
4.1 Async/Await原理
Async/Await本质是Generator + Promise 语法糖,核心是两者的协同工作。
1)当一个函数被标记为 async 时,JavaScript 引擎会将其转换成一个 Generator 函数,函数的返回值会被自动包装成 Promise。
2)Await利用生成器函数可以暂停和恢复这一特性,在等待异步操作时暂停函数执行,待异步操作完成后再恢复,让异步代码看起来像同步代码。
生成器函数Generator详情见:MDN上Generator章节Generator。
// Async函数返回Promise
async function hello() {
return "Hello";
}
// 等同于
function hello() {
return Promise.resolve("Hello");
}
console.log(hello() instanceof Promise); // true
4.2 Async/Await实现
实现思路:
1)基础结构:编写模拟函数,接收一个Generator函数作为参数,返回Promise(符合async函数返回)
2)自动执行:函数内部调用Generator函数,获取迭代器对象。递归逐步执行Generator的每一步;执行器自动处理yield/await的暂停和恢复
3)异步处理:当迭代器产生一个值(通常是 Promise)时,执行器会等待其完成,使用 Promise 的 then() 方法监听异步操作的结果,在异步操作完成后,将结果传回迭代器并继续下一步
4)恢复机制:异步操作成功后,自动调用迭代器的 next() 方法,传入结果值(模拟await表达式返回解析值)
5)异常处理:异步操作中异常使用迭代器的throw方法抛出,利用try-catch捕获;Generator 内部有 return 则提前结束执行并返回相应值
// 参数Generator函数generatorFunc
function asyncToGenerator(generatorFunc) {
return function() {
// 在当前的this上下文和参数下执行generatorFunc,并得到其迭代器对象(generator)
const generator = generatorFunc.apply(this, arguments);
// 返回Promise
return new Promise((resolve, reject) => {
//Generator 自动执行器中的单步执行函数
function step(key, arg) {
let result;
try {
// 动态调用 Generator 的方法
// key为next时,正常推进;key为throw时,抛回Generator内部
result = generator[key](arg);
} catch (error) {
return reject(error);
}
// Generator处理的结果跟状态 done为true则执行完毕,false则还有下一步
const { value, done } = result;
if (done) {
// 处理结束,返回值
return resolve(value);
} else {
// 处理未结束,执行并根据结果执行下一步
// 递归处理,实现自动执行
return Promise.resolve(value).then(
val => step('next', val),
err => step('throw', err)
);
}
}
// 开始单步处理
step('next');
});
};
}
// 测试使用
const asyncFunc = asyncToGenerator(function* () {
const result1 = yield new Promise(resolve =>
setTimeout(() => resolve('第一步完成'), 1000)
);
console.log(result1);
// 等待result1的结果出来后才会执行,实现await,异步变同步
const result2 = yield new Promise(resolve =>
setTimeout(() => resolve('第二步完成'), 1000)
);
console.log(result2);
return '全部完成';
});
asyncFunc().then(console.log);
五、实际项目应用场景
场景1:并发请求优化
// 错误的串行请求
async function slowFetch() {
const user = await fetch('/api/user');
const orders = await fetch('/api/orders');
const messages = await fetch('/api/messages');
// 总时间 = 3个请求时间之和
}
// 正确的并发请求
async function fastFetch() {
const [user, orders, messages] = await Promise.all([
fetch('/api/user'),
fetch('/api/orders'),
fetch('/api/messages')
]);
// 总时间 = 最慢的请求时间
}
场景2:请求失败重试机制
function fetchWithRetry(url, options = {}, maxRetries = 3) {
return new Promise(async (resolve, reject) => {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url, options);
if (response.ok) {
return resolve(await response.json());
}
throw new Error(`HTTP ${response.status}`);
} catch (error) {
if (attempt === maxRetries) {
reject(`已重连${maxRetries}次,错误是${error}`);
reject(error);
} else {
console.log(`请求失败,第${attempt}次重试...`);
await sleep(1000 * attempt); // 指数退避(延迟时间随重试次数增加)
}
}
}
});
}
// 延迟函数,sleep一段时间后返回resolve,便于进行下一次重连
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// 使用
fetchWithRetry('/api/data')
.then(console.log)
.catch(console.error);
场景3:防抖和节流优化
// 异步防抖函数
function asyncDebounce(func, wait) {
let timeoutId;
// 存储尚未完成的Promise的resolve回调
let pendingResolves = [];
return function(...args) {
return new Promise((resolve, reject) => {
// 清除之前的定时器
clearTimeout(timeoutId);
// 清除之前的pending状态,之前未完成Promise使用错误对象触发
pendingResolves.forEach(res => res(new Error('Debounced')));
pendingResolves = [];
// (防抖)设置新定时器
timeoutId = setTimeout(async () => {
try {
const result = await func.apply(this, args);
// 函数完成后结果传递给所有Promise进行回调
pendingResolves.forEach(res => res(result));
pendingResolves = [];
} catch (error) {
pendingResolves.forEach(res => rej(error));
pendingResolves = [];
}
}, wait);
// 将当前Promise的resolve回调存入pendingResolves数组
pendingResolves.push(resolve);
});
};
}
// 使用:输入时自动搜索,300ms内只执行最后一次
const search = asyncDebounce(async (query) => {
const response = await fetch(`/api/search?q=${query}`);
return response.json();
}, 300);
search('react').then(console.log);
六、面试常见问题解析
问题1:以下代码有什么问题?如何改进?(Async/Await错误处理)
async function getData() {
const data1 = await fetch('/api/data1');
const data2 = await fetch('/api/data2');
return { data1, data2 };
}
getData().then(console.log).catch(console.error);
问题:
1)两个请求是串行的,如果data1失败,data2永远不会执行
2)缺少错误处理,当请求失败时会导致未处理的 Promise 拒绝
优化:
async function getData() {
try {
const [data1, data2] = await Promise.all([
fetch('/api/data1'),
fetch('/api/data2')
]);
return { data1, data2 };
} catch (error) {
console.error('请求失败:', error);
throw error;
}
}
问题2: 请给出以下代码的执行顺序
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
Promise.resolve().then(() => console.log('5'));
console.log('6');
解题思路
同步代码:立即执行,console.log
任务队列:宏任务队列:setTimeout;微任务队列:Promise的then() 回调
应用事件循环规则:同步代码 → 所有微任务 → 第一个宏任务 → 所有微任务 → …
解答:
执行顺序:1 → 4 → 6 → 3 → 5 → 2
问题3: 并发控制与性能优化
题目:需要从 100 个 URL 获取数据,但为了避免服务器压力,要求最多同时发起 5 个请求。请实现这个并发控制器。
解题思路
要求100 个任务,并发数限制为 5
1)维护一个"执行中"的任务池
2)当池中有空位时,添加新任务
3)任务完成时,从池中移除并添加下一个
实现方案: Promise 和队列控制
async function onLineRequests(urls, maxConcurrent = 5) {
const results = [];
let currentIndex = 0;
// 创建一个线程池:线程数量为min(最大并发数, 实际任务数),每个线程都是一个独立的异步函数,并发启动
const workers = Array(Math.min(maxConcurrent, urls.length)).fill()
.map(async () => {
// 通过while实现自主工作循环
while (currentIndex < urls.length) {
// 通过 currentIndex++ 实现任务的安全分配(避免重复处理)
const url = urls[currentIndex++];
try {
const response = await fetch(url);
results.push(await response.json());
} catch (error) {
results.push({ error: error.message });
}
}
});
// 等待所有线程完成
await Promise.all(workers);
return results;
}
七、总结
了解异步编程底层原理,可以协助我们在多场景中能够使用异步编程的思想,解决问题。
✅ 推荐使用:
1)使用Async/Await代替回调金字塔
2)Promise.all()处理并发请求
3)合理的错误处理机制
4)使用防抖节流优化性能
❌ 不推荐使用:
避免循环中误用异步|不必要的嵌套|忽略错误处理
下期预告
下一次我们将深入探讨另一个JavaScript 的核心话题:浏览器事件模型和请求方式。
如果觉得有帮助,请关注+点赞,这是对我最大的鼓励!
如有问题,请评论区留言

被折叠的 条评论
为什么被折叠?



