引言

JavaScript 作为一门单线程语言,异步编程是其核心特性之一。从早期的回调函数到 Promise 对象,再到 ES2017 引入的 async/await 语法糖,JavaScript 异步编程的写法经历了巨大的演进。本文将系统梳理 JavaScript 异步编程的发展脉络,深入讲解 Promise 与 async/await 的原理与实践,并总结日常开发中的最佳实践。

一、回调函数时代的问题

在早期的 JavaScript 中,处理异步操作主要依赖回调函数。例如读取文件、发起网络请求等操作,都需要传入一个回调函数来处理结果:

// 典型的回调函数写法
function fetchData(callback) {
    setTimeout(() => {
        const data = { id: 1, name: 'example' };
        callback(null, data);
    }, 1000);
}

fetchData((err, data) => {
    if (err) {
        console.error('出错了:', err);
    } else {
        console.log('获取到数据:', data);
    }
});

当多个异步操作需要依次执行时,就会出现著名的"回调地狱"(Callback Hell):

// 回调地狱示例
getUserData(userId, (err, user) => {
    if (err) { handleError(err); return; }
    getOrders(user.id, (err, orders) => {
        if (err) { handleError(err); return; }
        getOrderDetail(orders[0].id, (err, detail) => {
            if (err) { handleError(err); return; }
            // 更多嵌套...
        });
    });
});

这种写法的问题很明显:代码横向发展而非纵向,可读性差、错误处理复杂、难以维护和调试。

二、Promise:异步编程的救星

2.1 Promise 基本概念

Promise 是 ES6 引入的异步编程解决方案,它代表一个异步操作的最终完成或失败。Promise 有三种状态:

  • pending(进行中):初始状态,既不是成功也不是失败
  • fulfilled(已成功):操作成功完成
  • rejected(已失败):操作失败

一旦 Promise 的状态从 pending 变为 fulfilled 或 rejected,就不会再改变,此时称为 settled(已定型)。

2.2 创建和消费 Promise

// 创建一个 Promise
const myPromise = new Promise((resolve, reject) => {
    setTimeout(() => {
        const success = true;
        if (success) {
            resolve('操作成功');
        } else {
            reject(new Error('操作失败'));
        }
    }, 1000);
});

// 消费 Promise
myPromise
    .then(result => {
        console.log(result); // 操作成功
        return processResult(result);
    })
    .then(processed => {
        console.log(processed);
    })
    .catch(error => {
        console.error('捕获错误:', error.message);
    })
    .finally(() => {
        console.log('无论成功失败都会执行');
    });

2.3 Promise 链式调用

Promise 最大的优势之一是通过链式调用解决了回调地狱问题:

// 使用 Promise 链式调用替代回调地狱
getUserData(userId)
    .then(user => getOrders(user.id))
    .then(orders => getOrderDetail(orders[0].id))
    .then(detail => {
        console.log('订单详情:', detail);
    })
    .catch(err => {
        console.error('任何一步出错都会被捕获:', err);
    });

链式调用的优势是代码呈纵向发展,结构清晰,并且只需在最末尾统一处理错误。

2.4 Promise 常用静态方法

Promise.all — 等待所有 Promise 完成:

const fetchUser = fetch('/api/user/1').then(r => r.json());
const fetchPosts = fetch('/api/posts?userId=1').then(r => r.json());
const fetchComments = fetch('/api/comments?postId=1').then(r => r.json());

// 并行请求,全部完成后统一处理
Promise.all([fetchUser, fetchPosts, fetchComments])
    .then(([user, posts, comments]) => {
        console.log('用户:', user);
        console.log('文章:', posts);
        console.log('评论:', comments);
    })
    .catch(err => {
        console.error('任一请求失败:', err);
    });

Promise.race — 返回最快完成的 Promise 结果:

// 请求超时控制
const timeout = new Promise((_, reject) => 
    setTimeout(() => reject(new Error('请求超时')), 5000)
);

Promise.race([fetch('/api/data'), timeout])
    .then(data => console.log('请求成功:', data))
    .catch(err => console.error(err.message)); // 5秒后输出"请求超时"

Promise.allSettled — 等待所有 Promise 完成(无论成功失败):

const urls = ['/api/a', '/api/b', '/api/c'];
const requests = urls.map(url => 
    fetch(url).then(r => r.json()).catch(err => ({ error: err.message }))
);

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

Promise.any — 返回第一个成功的 Promise:

const mirrors = [
    'https://mirror1.example.com/api/data',
    'https://mirror2.example.com/api/data',
    'https://mirror3.example.com/api/data'
];

Promise.any(mirrors.map(url => fetch(url)))
    .then(response => console.log('最快响应:', response))
    .catch(err => console.error('所有请求失败:', err));

三、async/await:异步编程的优雅写法

3.1 基本语法

async/await 是 ES2017 引入的语法糖,让异步代码看起来像同步代码一样直观:

// Promise 写法
function getOrderInfo() {
    return getUserId()
        .then(id => getUser(id))
        .then(user => getOrders(user.id))
        .then(orders => getOrderDetail(orders[0].id))
        .then(detail => {
            console.log(detail);
            return detail;
        })
        .catch(err => {
            console.error(err);
        });
}

// async/await 写法(逻辑完全等价)
async function getOrderInfo() {
    try {
        const id = await getUserId();
        const user = await getUser(id);
        const orders = await getOrders(user.id);
        const detail = await getOrderDetail(orders[0].id);
        console.log(detail);
        return detail;
    } catch (err) {
        console.error(err);
    }
}

使用 async/await 后,代码的流程和逻辑一目了然,不再需要层层嵌套的 .then() 调用。

3.2 错误处理模式

在 async/await 中,错误处理主要使用 try/catch:

// 模式一:顶层 try/catch
async function fetchData() {
    try {
        const data = await fetch('/api/data').then(r => r.json());
        return data;
    } catch (err) {
        console.error('请求失败:', err);
        throw err; // 向上传播
    }
}

// 模式二:单个 await 的 try/catch
async function fetchMixed() {
    let cachedData;
    try {
        cachedData = await loadFromCache();
    } catch {
        cachedData = null; // 缓存加载失败不阻断后续流程
    }
    
    const freshData = await fetchFreshData();
    return { ...cachedData, ...freshData };
}

// 模式三:封装为 Result 类型
async function safeFetch(url) {
    try {
        const response = await fetch(url);
        const data = await response.json();
        return { success: true, data };
    } catch (error) {
        return { success: false, error: error.message };
    }
}

// 使用
const result = await safeFetch('/api/user');
if (result.success) {
    console.log(result.data);
} else {
    console.error('出错:', result.error);
}

3.3 并发执行优化

async/await 虽然让代码看起来是同步执行,但如果多个异步操作之间没有依赖关系,仍然需要使用 Promise.all 并行执行:


// 错误写法:串行执行(总耗时约 3 秒)
async function getDashboardData_bad() {
    const users = await fetchUsers();      // 等待 1 秒
    const orders = await fetchOrders();    // 等待 1 秒
    const stats = await fetchStats();      // 等待 1 秒
    return { users, orders, stats };
}

// 正确写法:并行执行(总耗时约 1 秒)
async function getDashboardData_good() {
    const [users, orders, stats] = await Promise.all([
        fetchUsers(),
        fetchOrders(),
        fetchStats()
    ]);
    return { users, orders, stats };
}

// 混合写法:部分有依赖,部分可并行
async function getReport() {
    const user = await fetchUser(userId);              // 先获取用户
    const [orders, preferences] = await Promise.all([
        fetchOrders(user.id),                          // 并行
        fetchPreferences(user.id)                       // 并行
    ]);
    return generateReport(user, orders, preferences);
}

四、实战场景

4.1 带重试的网络请求

async function fetchWithRetry(url, options = {}, maxRetries = 3, delay = 1000) {
    for (let attempt = 0; attempt <= maxRetries; attempt++) {
        try {
            const response = await fetch(url, options);
            if (!response.ok) throw new Error(`HTTP ${response.status}`);
            return await response.json();
        } catch (error) {
            if (attempt === maxRetries) throw error;
            console.warn(`第 ${attempt + 1} 次重试...`);
            await new Promise(resolve => setTimeout(resolve, delay * (attempt + 1)));
        }
    }
}

// 使用:最多重试3次,间隔递增
const data = await fetchWithRetry('/api/critical-data', {}, 3, 1000);

4.2 异步迭代器与异步生成器

// 异步生成器:按需分页获取数据
async function* fetchPaginatedData(baseUrl) {
    let page = 1;
    let hasMore = true;
    
    while (hasMore) {
        const response = await fetch(`${baseUrl}?page=${page}`);
        const data = await response.json();
        
        yield data.items; // 每次 yield一页数据
        
        hasMore = data.hasNextPage;
        page++;
    }
}

// 消费异步生成器
async function processAllPages() {
    const generator = fetchPaginatedData('/api/articles');
    
    for await (const items of generator) {
        for (const item of items) {
            await processItem(item);
        }
    }
}

4.3 Promise 取消与 AbortController

// 使用 AbortController 取消异步请求
let controller;

async function startLongRequest() {
    controller = new AbortController();
    const { signal } = controller;
    
    try {
        const response = fetch('/api/heavy-computation', { signal });
        const data = await response;
        return data;
    } catch (err) {
        if (err.name === 'AbortError') {
            console.log('请求已被取消');
        } else {
            throw err;
        }
    }
}

// 取消请求
function cancelRequest() {
    if (controller) {
        controller.abort();
    }
}

// 实战应用:搜索防抖 + 取消旧请求
let searchController;

async function handleSearch(keyword) {
    if (searchController) {
        searchController.abort(); // 取消之前的搜索
    }
    searchController = new AbortController();
    
    try {
        const results = await fetch(`/api/search?q=${keyword}`, {
            signal: searchController.signal
        });
        renderResults(await results.json());
    } catch (err) {
        if (err.name !== 'AbortError') throw err;
    }
}

五、最佳实践总结

  1. 优先使用 async/await:代码更清晰直观,便于维护和调试
  2. 善用 Promise.all:无依赖的异步操作一律并行执行
  3. 记住 Promise 一旦创建就立即执行:如果不希望立即执行,将其包装在函数中
  4. 永远不要在非 async 函数中遗漏 .catch():未捕获的 Promise 拒绝可能导致程序静默失败
  5. 循环中的 async 注意性能:for 循环中 await 是串行,如需并行使用 Promise.all + map
  6. async 函数始终返回 Promise:即使 return 一个普通值,也会被自动包装为 Promise
  7. 使用 AbortController 配合场景:长时间运行的异步操作(大文件上传、复杂计算)应支持取消

结语

JavaScript 异步编程从回调函数到 Promise 再到 async/await,每一步演进都是为了让代码更优雅、更可靠。理解 Promise 的底层机制(状态机、微任务队列)对于写出健壮的异步代码至关重要,而 async/await 则是实践中最高效的编码方式。在实际项目中,合理运用本文介绍的模式和技巧,能够显著提升代码质量和开发效率。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部