讲解一下Promise与async/await之间的故事

33 阅读4分钟
在JS的异步世界里,Promise和async/await就像咖啡和咖啡因,一个是实体,一个是更便捷的摄入方式
首先,想象一下你去咖啡店点单:
Promise是服务员给了你一张排队的小票(它就是Promise的then方法),说咖啡好了会震动提醒你,
而async/await是VIP通道,服务员会当场原地给你做好之后直接递给你
前者是让你在等待时还能刷会儿手机,后者就是让你代码同步的爽感

很多人以为async/await是Promise的替代品,其实这个说法是错误的。async/await的本质是Promise的语法糖,他的底层依然是用Promise去实现的,async/await会让异步代码看起来更像同步代码

在写法上来说:
Promise写法
fetchData().then(res => {
    return fetchNewData(res)
}).then(result => {
    console.log(result)
}).catch(err => {
    console.log(err)
})

async/await写法
async function handleData(){
    try{
        const data = await fetchData()
        const result = await fetchNewData(data)
        console.log(result)
    }catch(err) {
        console.log(err)
    }
}

从这里看,其实两者的功能是完全一致的,但是对于async/await这样的写法来说,看起来会更符合人的阅读习惯。

那么我们针对这两种情况,各自的使用时机是什么呢?

对于线性异步流程来说
就是一共需要请求三次,每次都会使用上一次返回的结果作为参数去请求下一次
比如:用户登录后获取token,再通过token获取用户信息,在根据用户信息去获取对应的角色权限
async function getApp(username, password) {
    try{
        const token = await login(username, password)
        const userInfo = await getUserInfo(token)
        const permissions = await getPermissions(userInfo.role)
        render(permissions)
    } catch(err) {
        console.log(err)
    }
}

对于包含条件判断的异步逻辑的话
async function checkAndUpdateAuth() {
    const appVersion = await getAppVersion()
    if(appVersion === '1.8'){
        const newInfo = await getNewInfo()
        if(newInfo.force){
            await updateInfo()
        } else {
            await isUpdateInfo()
        }
    }
}

对于需要中断执行的场景来说
async function submitForm(data){
    if(!data.password) {
        showError('密码不能为空')
        return
    }
    try{
        const validateResult = await validateForm(data)
        if(validateResult.errno !== 0) {
                showError(validateResult.errmsg)
                return
        }
        await submitForm(data)
    } catch(err) {
        handleError(err)
    }
}
虽然使用async/await会方便一些,但是在有些场景下,使用Promise的原生API会更适合一点

当需要并行执行多个异步操作时
当我们需要执行多个异步任务,并且这些异步任务互相不依赖时,我们使用Promise.all并行执行能够大幅的提高效率,比如我们要同时获取列表数据以及筛选项的状态枚举
async function loadDashBoard() {
    const [list, categories] = await Promise.all([
        fetchList()
        fetchCategories()
    ])
    renderList(list)
    renderFilters(categories)
}
如果使用await的话,就变成串行执行了,总耗时就会增加
const list = await fetchList()
const categories = await fetchCategories()

当我们需要超时控制的异步操作时
Promise.race方法会使用最采用最先改变状态的Promise结果,谁先成功/失败就采用谁的结果,所以可以用来实现,如果一个结果三秒内没返回结果就提示请求超时。
// 封装一个带超时的异步函数
function withTimeout(promise, timeoutMs = 3000) {
    let timer;  // 将timer提升到外部作用域
    const timeoutPromise = new Promise((_, reject) => {
        timer = setTimeout(() => {
            reject(new Error('请求超时'));
        }, timeoutMs);
    });
    return Promise.race([
        promise,
        timeoutPromise
    ]).finally(() => clearTimeout(timer));  // 确保始终清除定时器
}

// 使用
async function loadData() {
    try {
        const data = await withTimeout(fetchLargeData());
        render(data);
    } catch (err) {
        showError(err.message);  // 可能是超时错误
    }
}

再一次就是当我们处理动态数量的异步任务时
比如我们批量上传文件时
async function uploadFiles(files) {
    if (!files?.length) return;  // 增加空值判断,避免空数组或undefined时执行无效操作

    // 生成一个包含所有上传Promise的数组
    const uploadPromises = files.map(file => {
    return uploadFile(file);  // 每个文件的上传是异步操作
});

    // 等待所有文件上传完成
    const results = await Promise.all(uploadPromises);

    // 处理结果
    const successCount = results.filter(r => r?.success).length;  // 增加可选链容错
    showMessage(`成功上传 ${successCount}/${files.length} 个文件`);
}

在这些动态场景下,Promise的API会比async/await更高效

而在实际开发中,对于两者通常采用结合使用的方式,会带来更好的效果。比如先并行获取枚举数据,然后再串行处理依赖关系
async function buildReport() {
    // 第一步:并行获取不相关的数据(提高效率)
    const [users, orders, products] = await Promise.all([
        fetchUsers(),
        fetchOrders(),
        fetchProducts()
    ]);

    // 第二步:串行处理依赖关系的逻辑
    const userStats = await calculateUserStats(users);
    const orderSummary = await generateOrderSummary(orders, userStats);  // 依赖userStats
    const report = await compileReport(orderSummary, products);  // 依赖前两者

    return report;
}
这段代码先用 Promise.all 并行请求,节省时间;再用 async/await 处理有依赖的串行逻辑,兼顾效率和可读性。

所以我们具体问题具体分析,具体实践
针对不同的问题去采用不同的处理方式:
对于顺序操作的异步任务通常更采用async/await的方式
而对于批量解决或者是进行超时控制时,采用Promise的API会更好一点