JavaScript异步编程

掌握回调、Promise、async/await等异步处理技术

什么是异步编程?

异步编程允许代码在等待某些操作(如网络请求、文件读取等)完成时继续执行其他任务,而不是阻塞整个程序。这对于构建响应式的Web应用至关重要。

JavaScript的单线程模型

JavaScript是单线程语言,这意味着它一次只能执行一个任务。异步编程通过事件循环(Event Loop)机制实现非阻塞操作。

同步 vs 异步

特性 同步编程 异步编程
执行方式 顺序执行,阻塞后续代码 非阻塞,后续代码立即执行
性能 可能造成界面冻结 更好的响应性和用户体验
复杂度 简单直观 需要处理回调、Promise等
适用场景 简单计算、快速操作 I/O操作、网络请求、定时任务

JavaScript事件循环

事件循环是JavaScript处理异步操作的核心机制,它由调用栈、任务队列和微任务队列组成。

事件循环流程

  1. 执行同步代码(调用栈)
  2. 处理微任务队列(Promise回调等)
  3. 处理宏任务队列(setTimeout、事件回调等)
  4. 重复上述过程
事件循环示例
console.log('1. 同步代码开始');

// 宏任务
setTimeout(() => {
    console.log('6. 宏任务执行');
}, 0);

// Promise(微任务)
Promise.resolve()
    .then(() => {
        console.log('4. 微任务1执行');
        return '结果';
    })
    .then((value) => {
        console.log('5. 微任务2执行,收到:', value);
    });

console.log('2. 同步代码继续');

// 另一个微任务
queueMicrotask(() => {
    console.log('3. 微任务执行');
});

console.log('7. 同步代码结束');

// 输出顺序:
// 1. 同步代码开始
// 2. 同步代码继续
// 7. 同步代码结束
// 4. 微任务1执行
// 3. 微任务执行
// 5. 微任务2执行,收到: 结果
// 6. 宏任务执行

微任务 vs 宏任务

类型 示例 执行时机
微任务 Promise.then/catch/finally, queueMicrotask, MutationObserver 在当前任务结束后,下一个任务开始前
宏任务 setTimeout, setInterval, setImmediate, I/O操作, UI渲染 在下一个事件循环迭代中

回调函数(Callback)

回调函数是异步编程的基础,它是一个在另一个函数执行完成后被调用的函数。

回调函数示例
// 简单的回调函数
function fetchData(callback) {
    setTimeout(() => {
        const data = { name: '张三', age: 25 };
        callback(null, data); // 第一个参数通常是错误对象
    }, 1000);
}

// 使用回调函数
fetchData((error, data) => {
    if (error) {
        console.error('错误:', error);
    } else {
        console.log('数据:', data);
    }
});

// Node.js风格回调(错误优先)
function readFile(path, callback) {
    // 模拟文件读取
    setTimeout(() => {
        const success = Math.random() > 0.2;
        if (success) {
            callback(null, `文件 ${path} 的内容`);
        } else {
            callback(new Error(`无法读取文件: ${path}`));
        }
    }, 500);
}

// 使用示例
readFile('/path/to/file.txt', (err, data) => {
    if (err) {
        console.error('错误:', err.message);
        return;
    }
    console.log('文件内容:', data);
});

// 回调地狱示例(不推荐)
function getUserData(userId, callback) {
    getUser(userId, (error, user) => {
        if (error) return callback(error);
        
        getPosts(user.id, (error, posts) => {
            if (error) return callback(error);
            
            getComments(posts[0].id, (error, comments) => {
                if (error) return callback(error);
                
                callback(null, { user, posts, comments });
            });
        });
    });
}

回调地狱(Callback Hell): 多层嵌套的回调函数会导致代码难以阅读和维护,这就是所谓的"回调地狱"或"金字塔厄运"。

Promise

Promise是ES6引入的异步编程解决方案,解决了回调地狱的问题。Promise表示一个异步操作的最终完成(或失败)及其结果值。

Promise的生命周期

  • pending(待定):初始状态,既没有被兑现,也没有被拒绝
  • fulfilled(已兑现):操作成功完成
  • rejected(已拒绝):操作失败
Promise基础
// 创建Promise
const promise = new Promise((resolve, reject) => {
    // 异步操作
    setTimeout(() => {
        const success = Math.random() > 0.5;
        if (success) {
            resolve('操作成功!'); // 状态变为fulfilled
        } else {
            reject('操作失败!');  // 状态变为rejected
        }
    }, 1000);
});

// 使用Promise
promise
    .then(result => {
        console.log('成功:', result);
        return result.toUpperCase(); // 可以返回新值
    })
    .then(uppercaseResult => {
        console.log('处理后的结果:', uppercaseResult);
    })
    .catch(error => {
        console.error('失败:', error);
    })
    .finally(() => {
        console.log('操作完成'); // 无论成功失败都会执行
    });

// Promise链式调用
function getUser(userId) {
    return new Promise((resolve) => {
        setTimeout(() => resolve({ id: userId, name: '用户' + userId }), 500);
    });
}

function getUserPosts(userId) {
    return new Promise((resolve) => {
        setTimeout(() => resolve([`用户${userId}的帖子1`, `用户${userId}的帖子2`]), 300);
    });
}

getUser(1)
    .then(user => {
        console.log('用户:', user);
        return getUserPosts(user.id); // 返回新的Promise
    })
    .then(posts => {
        console.log('用户的帖子:', posts);
    })
    .catch(error => {
        console.error('错误:', error);
    });

Promise的静态方法

Promise工具方法
// Promise.resolve - 创建已解决的Promise
const resolvedPromise = Promise.resolve('立即解决的值');
resolvedPromise.then(value => console.log(value)); // "立即解决的值"

// Promise.reject - 创建已拒绝的Promise
const rejectedPromise = Promise.reject('立即拒绝的原因');
rejectedPromise.catch(reason => console.error(reason)); // "立即拒绝的原因"

// Promise.all - 所有Promise都成功
const promises = [
    Promise.resolve('结果1'),
    Promise.resolve('结果2'),
    Promise.resolve('结果3')
];

Promise.all(promises)
    .then(results => {
        console.log('所有结果:', results); // ['结果1', '结果2', '结果3']
    })
    .catch(error => {
        console.error('有一个失败:', error); // 如果任一Promise失败
    });

// Promise.race - 第一个完成(成功或失败)的Promise
Promise.race([
    fetch('/api/slow'),
    fetch('/api/fast'),
    new Promise((_, reject) => 
        setTimeout(() => reject(new Error('超时')), 5000)
    )
])
.then(result => console.log('最快的结果:', result))
.catch(error => console.error('错误:', error));

// Promise.allSettled - 所有Promise都完成(无论成功失败)
Promise.allSettled([
    Promise.resolve('成功'),
    Promise.reject('失败'),
    Promise.resolve('另一个成功')
])
.then(results => {
    results.forEach(result => {
        if (result.status === 'fulfilled') {
            console.log('成功:', result.value);
        } else {
            console.log('失败:', result.reason);
        }
    });
});

// Promise.any - 第一个成功的Promise(ES2021)
Promise.any([
    Promise.reject('错误1'),
    Promise.resolve('成功1'),
    Promise.resolve('成功2')
])
.then(firstSuccess => {
    console.log('第一个成功的结果:', firstSuccess); // "成功1"
})
.catch(error => {
    console.error('全部失败:', error);
});

async/await

async/await是ES2017引入的语法糖,让异步代码看起来像同步代码,基于Promise构建,但提供了更清晰的语法。

async/await用法
// async函数总是返回Promise
async function fetchUserData(userId) {
    try {
        const user = await getUser(userId);        // 等待Promise解决
        const posts = await getPosts(user.id);     // 等待另一个Promise
        const comments = await getComments(posts[0].id);
        
        return { user, posts, comments };          // 自动包装为Promise
    } catch (error) {
        console.error('获取数据失败:', error);
        throw error; // 重新抛出错误
    }
}

// 使用async函数
fetchUserData(1)
    .then(data => console.log('用户数据:', data))
    .catch(error => console.error('错误:', error));

// 立即执行async函数
(async function() {
    try {
        const data = await fetchUserData(1);
        console.log('立即执行结果:', data);
    } catch (error) {
        console.error('立即执行错误:', error);
    }
})();

// 在类方法中使用
class UserService {
    async getUserProfile(userId) {
        const user = await this.fetchUser(userId);
        const posts = await this.fetchPosts(userId);
        return { user, posts };
    }
    
    async fetchUser(userId) {
        // 模拟API调用
        return new Promise(resolve => {
            setTimeout(() => resolve({ id: userId, name: `用户${userId}` }), 500);
        });
    }
}

// 并行执行多个异步操作
async function fetchAllData(userId) {
    // 使用Promise.all并行执行
    const [user, posts, settings] = await Promise.all([
        getUser(userId),
        getPosts(userId),
        getSettings(userId)
    ]);
    
    return { user, posts, settings };
}

// 在循环中使用await
async function processUsers(userIds) {
    const results = [];
    for (const id of userIds) {
        const user = await getUser(id); // 顺序执行
        results.push(user);
    }
    return results;
}

// 并行处理循环(更高效)
async function processUsersParallel(userIds) {
    const promises = userIds.map(id => getUser(id));
    return await Promise.all(promises); // 并行执行
}

async/await最佳实践

  • 总是使用try/catch处理错误
  • 使用Promise.all并行执行独立操作
  • 避免不必要的await
  • 在循环中谨慎使用await,考虑性能影响

高级异步模式

1. 异步生成器

异步生成器
// 异步生成器函数
async function* asyncGenerator() {
    let i = 0;
    while (i < 3) {
        // 模拟异步操作
        await new Promise(resolve => setTimeout(resolve, 1000));
        yield i++;
    }
}

// 使用for await...of循环
(async () => {
    for await (const value of asyncGenerator()) {
        console.log(value); // 0, 1, 2 (每秒输出一个)
    }
})();

// 分页数据的异步生成器
async function* paginatedData(url) {
    let page = 1;
    let hasMore = true;
    
    while (hasMore) {
        const response = await fetch(`${url}?page=${page}`);
        const data = await response.json();
        
        yield data.items;
        
        hasMore = data.hasMore;
        page++;
    }
}

// 使用分页数据
(async () => {
    for await (const items of paginatedData('/api/data')) {
        console.log('收到数据:', items);
        // 处理每一页的数据
    }
})();

2. 取消异步操作

可取消的Promise
// 创建可取消的Promise
function cancellablePromise(executor) {
    let cancel;
    const promise = new Promise((resolve, reject) => {
        cancel = (reason = '操作已取消') => {
            reject(new Error(reason));
        };
        executor(resolve, reject, () => cancel);
    });
    
    return { promise, cancel };
}

// 使用示例
const { promise, cancel } = cancellablePromise((resolve, reject, onCancel) => {
    const timer = setTimeout(() => resolve('操作完成'), 5000);
    
    // 注册取消回调
    onCancel(() => {
        clearTimeout(timer);
        console.log('清理资源...');
    });
});

// 在3秒后取消
setTimeout(() => {
    cancel('用户取消了操作');
}, 3000);

promise
    .then(result => console.log('成功:', result))
    .catch(error => console.error('失败:', error.message));

// 使用AbortController(现代方法)
function fetchWithTimeout(url, options = {}, timeout = 5000) {
    const controller = new AbortController();
    const { signal } = controller;
    
    const timeoutId = setTimeout(() => {
        controller.abort();
    }, timeout);
    
    return fetch(url, {
        ...options,
        signal
    }).finally(() => {
        clearTimeout(timeoutId);
    });
}

// 使用示例
fetchWithTimeout('/api/data', {}, 3000)
    .then(response => response.json())
    .then(data => console.log('数据:', data))
    .catch(error => {
        if (error.name === 'AbortError') {
            console.log('请求超时');
        } else {
            console.error('其他错误:', error);
        }
    });

3. 重试机制

带重试的异步操作
// 带指数退避的重试函数
async function retryWithBackoff(operation, maxRetries = 3, baseDelay = 1000) {
    let lastError;
    
    for (let attempt = 0; attempt <= maxRetries; attempt++) {
        try {
            return await operation();
        } catch (error) {
            lastError = error;
            
            if (attempt === maxRetries) {
                break;
            }
            
            // 指数退避:延迟时间随尝试次数增加
            const delay = baseDelay * Math.pow(2, attempt);
            console.log(`尝试 ${attempt + 1} 失败,${delay}ms后重试...`);
            
            await new Promise(resolve => setTimeout(resolve, delay));
        }
    }
    
    throw lastError;
}

// 使用示例
async function fetchData() {
    return retryWithBackoff(
        async () => {
            const response = await fetch('/api/unstable');
            if (!response.ok) {
                throw new Error(`HTTP ${response.status}`);
            }
            return response.json();
        },
        3,  // 最多重试3次
        1000 // 基础延迟1秒
    );
}

fetchData()
    .then(data => console.log('最终成功:', data))
    .catch(error => console.error('最终失败:', error.message));

实践练习

👆 请点击上方按钮进行演示操作

选择不同的演示按钮来探索JavaScript异步编程的各种功能和用法

性能优化和最佳实践

1. 避免常见的性能陷阱

性能提示:

  • 使用Promise.all并行执行独立操作
  • 避免在循环中使用await,考虑使用Promise.all
  • 合理使用缓存减少重复请求
  • 设置适当的超时时间
  • 使用防抖和节流控制高频操作

2. 错误处理策略

全面的错误处理
// 包装异步操作,提供更好的错误信息
async function executeWithContext(operation, context = '') {
    try {
        return await operation();
    } catch (error) {
        // 添加上下文信息
        error.context = context;
        error.timestamp = new Date().toISOString();
        console.error(`操作失败 [${context}]:`, error);
        throw error;
    }
}

// 使用示例
async function loadPageData() {
    const [user, posts] = await Promise.all([
        executeWithContext(() => fetchUser(123), '获取用户信息'),
        executeWithContext(() => fetchPosts(123), '获取用户帖子')
    ].map(p => p.catch(error => {
        // 单个失败不影响其他操作
        console.error('部分数据加载失败:', error.context);
        return null;
    })));
    
    return { user, posts };
}

// 全局错误处理
window.addEventListener('unhandledrejection', event => {
    console.error('未处理的Promise拒绝:', event.reason);
    // 可以在这里报告错误到监控系统
    event.preventDefault();
});

浏览器兼容性

特性 Chrome Firefox Safari Edge IE
Promise 32+ 29+ 8+ 12+ 11-
async/await 55+ 52+ 10.1+ 15+ 11-
Promise.allSettled 76+ 71+ 13+ 79+ 11-
Promise.any 85+ 79+ 14+ 85+ 11-

兼容性提示: 对于不支持现代异步特性的旧浏览器,可以使用Babel等转译工具,或引入polyfill如core-js。

下一步学习

掌握了异步编程后,可以继续学习: