深入浅出:Promise与async/await,让异步编程更优雅

271 阅读7分钟

从回调地狱到Promise链,再到async/await的同步写法,JavaScript异步编程的进化之路

大家好,我是你们的技术小伙伴FogLetter,今天我们来聊聊JavaScript异步编程的那些事儿。作为一个在掘金上分享过不少技术文章的老司机,我深知异步编程是每个JS开发者都必须掌握的技能。让我们一起来探索Promise和async/await的奇妙世界吧!

异步编程的进化史

还记得早期的JavaScript异步编程吗?满屏的回调函数,嵌套层级深不见底:

getData(function(a) {
    getMoreData(a, function(b) {
        getMoreData(b, function(c) {
            getMoreData(c, function(d) {
                getMoreData(d, function(e) {
                    // 这就是传说中的"回调地狱"
                });
            });
        });
    });
});

这种代码不仅难以阅读,错误处理也极其困难。然后Promise出现了,给我们带来了曙光。

Promise:异步编程的基石

什么是Promise?

Promise是JavaScript中处理异步操作的对象,它代表一个尚未完成但预期会完成的操作。简单来说,Promise就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。

Promise有三种状态:

  • pending:初始状态,既不是成功,也不是失败
  • fulfilled:操作成功完成
  • rejected:操作失败

状态一旦改变,就不会再变:只能从pending变为fulfilled或者从pending变为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);
});

Promise的并行处理:all、race、any、allSettled

Promise真正强大的地方在于它提供了一系列处理多个异步操作的静态方法。让我们通过一个生动的例子来理解它们:

// 模拟几个异步操作
const p1 = Promise.resolve('p1立即完成');
const p2 = new Promise((resolve) => {
    setTimeout(() => resolve('p2 1秒后完成'), 1000);
});
const p3 = new Promise((resolve) => {
    setTimeout(() => resolve('p3 2秒后完成'), 2000);
});
const p4 = Promise.reject('p4立即拒绝');
const p5 = new Promise((resolve, reject) => {
    setTimeout(() => reject('p5 1.5秒后拒绝'), 1500);
});

1. Promise.all() - 全成功才成功

console.time('Promise.all总耗时');
Promise.all([p1, p2, p3])
    .then(results => {
        console.log('全部成功:', results);
        console.timeEnd('Promise.all总耗时');
    })
    .catch(error => {
        console.log('有一个失败:', error);
        console.timeEnd('Promise.all总耗时');
    });

// 输出:
// 全部成功: ["p1立即完成", "p2 1秒后完成", "p3 2秒后完成"]
// Promise.all总耗时: 2002.345947265625ms

特点

  • 所有Promise都成功,才成功
  • 任何一个失败,立即失败
  • 适合多个异步操作都成功才能继续的场景

2. Promise.race() - 谁快听谁的

Promise.race([p2, p3, p5])
    .then(result => {
        console.log('第一个完成的是:', result);
    })
    .catch(error => {
        console.log('第一个失败的是:', error);
    });

// 输出:第一个完成的是: p2 1秒后完成

特点

  • 哪个Promise最先完成(无论成功或失败),就采用它的结果
  • 适合设置超时机制

3. Promise.any() - 首个成功即成功

Promise.any([p4, p5, p2])
    .then(result => {
        console.log('第一个成功的是:', result);
    })
    .catch(errors => {
        console.log('全部失败了:', errors);
    });

// 输出:第一个成功的是: p2 1秒后完成

特点

  • 只要有一个成功,就成功
  • 只有全部失败,才失败
  • 适合多个备用方案,只要一个成功即可的场景

4. Promise.allSettled() - 全部完成才结束

Promise.allSettled([p1, p4, p5])
    .then(results => {
        results.forEach((result, index) => {
            if (result.status === 'fulfilled') {
                console.log(`p${index+1}成功:`, result.value);
            } else {
                console.log(`p${index+1}失败:`, result.reason);
            }
        });
    });

// 输出:
// p1成功: p1立即完成
// p2失败: p4立即拒绝
// p3失败: p5 1.5秒后拒绝

特点

  • 等待所有Promise完成(无论成功或失败)
  • 返回每个Promise的结果数组
  • 适合需要知道所有操作结果的场景

手写实现Promise.all

理解了Promise.all的用法,我们来尝试自己实现一个:

Promise.MyAll = function(promises) {
    let results = []; // 存储每个Promise的结果
    let completed = 0; // 已完成的数量
    
    return new Promise((resolve, reject) => {
        promises.forEach((promise, index) => {
            // 用Promise.resolve包装,确保处理的是Promise对象
            Promise.resolve(promise)
                .then(result => {
                    results[index] = result; // 按顺序存储结果
                    completed++;
                    
                    // 全部完成,解析结果
                    if (completed === promises.length) {
                        resolve(results);
                    }
                })
                .catch(error => {
                    // 任何一个失败,立即拒绝
                    reject(error);
                });
        });
        
        // 处理空数组的情况
        if (promises.length === 0) {
            resolve(results);
        }
    });
};

// 测试我们实现的MyAll
Promise.MyAll([p1, p2, p3])
    .then(results => {
        console.log('MyAll结果:', results);
    })
    .catch(error => {
        console.log('MyAll错误:', error);
    });

async/await:异步编程的终极解决方案

虽然Promise解决了回调地狱的问题,但then()链式调用仍然不够直观。ES2017引入了async/await,让异步代码看起来像同步代码一样简洁。

什么是async/await?

async/await是基于Promise的语法糖,它允许我们以同步的方式编写异步代码。

  • async:声明一个函数是异步的,该函数总会返回一个Promise
  • await:暂停async函数的执行,等待Promise解决,然后继续执行

基本用法

// 传统Promise写法
function fetchData() {
    return fetch('/api/data')
        .then(response => response.json())
        .then(data => {
            return processData(data);
        })
        .catch(error => {
            console.error('错误:', error);
        });
}

// async/await写法
async function fetchData() {
    try {
        const response = await fetch('/api/data');
        const data = await response.json();
        return processData(data);
    } catch (error) {
        console.error('错误:', error);
    }
}

是不是清晰多了?async/await让代码可读性大大提升。

async/await的实现原理

你可能想知道async/await是如何工作的。本质上,它是Generator函数和Promise的组合:

// 模拟async函数的实现
function asyncGenerator(genFunc) {
    return function() {
        const generator = genFunc.apply(this, arguments);
        
        return new Promise((resolve, reject) => {
            function step(key, arg) {
                let result;
                try {
                    result = generator[key](arg);
                } catch (error) {
                    return reject(error);
                }
                
                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 myAsync = asyncGenerator(function* () {
    const a = yield Promise.resolve(1);
    const b = yield Promise.resolve(2);
    return a + b;
});

myAsync().then(result => console.log(result)); // 输出: 3

当然,实际实现比这复杂得多,但基本原理类似。Babel等转译器就是将async/await转换为类似的代码。

不要滥用async/await

虽然async/await很强大,但不能滥用。最常见的错误是在不需要顺序执行时也使用await:

// 错误用法 - 顺序执行,耗时更长
async function fetchSequential() {
    const user = await fetchUser(); // 等待用户数据
    const posts = await fetchPosts(); // 然后等待文章数据
    const comments = await fetchComments(); // 然后等待评论数据
    
    return { user, posts, comments };
}

// 正确用法 - 并行执行,效率更高
async function fetchParallel() {
    const [user, posts, comments] = await Promise.all([
        fetchUser(),
        fetchPosts(),
        fetchComments()
    ]);
    
    return { user, posts, comments };
}

经验法则

  • 多个异步操作有依赖关系时,使用async/await顺序执行
  • 多个异步操作相互独立时,使用Promise.all并行执行

实战技巧与常见陷阱

1. 错误处理

async/await的错误处理有多种方式:

// 方式1:try/catch
async function fetchData() {
    try {
        const data = await fetch('/api/data');
        return processData(data);
    } catch (error) {
        console.error('获取数据失败:', error);
        // 处理错误或重新抛出
        throw new Error('数据处理失败');
    }
}

// 方式2:catch方法
async function fetchData() {
    const data = await fetch('/api/data').catch(error => {
        console.error('获取数据失败:', error);
        return fallbackData; // 返回备用数据
    });
    
    return processData(data);
}

// 方式3:在调用处处理
async function main() {
    const dataPromise = fetchData();
    // 其他操作...
    
    try {
        const data = await dataPromise;
        console.log('数据:', data);
    } catch (error) {
        console.error('操作失败:', error);
    }
}

2. 循环中的await

在循环中使用await需要注意:

// 顺序执行 - 每个await等待前一个完成
async function processSequentially(items) {
    for (const item of items) {
        await processItem(item); // 顺序处理
    }
}

// 并行执行 - 同时处理所有项目
async function processInParallel(items) {
    const promises = items.map(item => processItem(item));
    await Promise.all(promises); // 并行处理
}

3. 顶层await

在ES2022中,我们可以在模块的顶层使用await:

// 模块加载时等待数据加载
const data = await fetch('/api/data').then(r => r.json());

export function getData() {
    return data;
}

总结

Promise和async/await是JavaScript异步编程的强大工具:

  1. Promise 提供了处理异步操作的标准方法和并行处理能力
  2. async/await 让异步代码看起来像同步代码,提高了可读性和可维护性
  3. 根据场景选择合适的工具:有依赖关系的异步操作使用async/await,独立的异步操作使用Promise.all

记住,技术是为人服务的,选择最适合当前场景的方法,而不是盲目追求最新特性。希望这篇文章能帮助你更好地理解和使用Promise和async/await!

如果你有任何问题或想法,欢迎在评论区留言讨论。记得点赞收藏哦,我们下期再见!


互动环节:你在使用Promise或async/await时遇到过什么有趣的问题或坑?分享在评论区,我们一起讨论解决方案!