深度解密JavaScript异步编程:从入门到精通一站式搞定

今天我们来聊一聊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 = 0return 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 的核心话题:浏览器事件模型和请求方式

如果觉得有帮助,请关注+点赞,这是对我最大的鼓励!
如有问题,请评论区留言

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序媛小王ouc

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值