事件循环与异步JavaScript
概述
JavaScript是单线程的,意味着它按单一序列执行代码。但多亏了事件循环和异步特性如setTimeout、Promises和async/await,它可以处理I/O操作、定时器和HTTP请求等任务而不会阻塞执行。
🔁 事件循环:核心概念
事件循环允许JavaScript通过将非阻塞操作放入队列并继续执行其他代码来执行非阻塞操作。当调用栈清空时,它检查队列并处理下一个任务。
🧠 涉及的组件
调用栈(Call Stack)
JavaScript代码执行的地方。它是LIFO(后进先出)。
Web APIs(由浏览器提供)
处理定时器、DOM事件、fetch请求等。
回调队列(任务队列)
存储要在栈清空后执行的回调函数。
微任务队列(Microtask Queue)
用于promises和queueMicrotask。比回调队列具有更高的优先级。
🔄 工作原理(简化流程)
- console.log("Start");
- setTimeout(() => {
- console.log("Timeout");
- }, 0);
- Promise.resolve().then(() => {
- console.log("Promise");
- });
- console.log("End");
- // 输出:
- // Start
- // End
- // Promise
- // Timeout
✅ 为什么?
console.log("Start") → 同步 → 立即执行
setTimeout(...) → 发送到Web APIs → 延迟后将回调发送到任务队列
Promise.then(...) → 微任务队列
console.log("End") → 同步 → 执行
微任务队列运行:Promise
任务队列运行:Timeout
📘 异步JavaScript工具
1. setTimeout / setInterval – 调度任务
- // 基本用法
- setTimeout(() => {
- console.log('3秒后执行');
- }, 3000);
- // 清除定时器
- const timerId = setTimeout(() => {
- console.log('这个不会执行');
- }, 5000);
- clearTimeout(timerId);
- // 间隔执行
- const intervalId = setInterval(() => {
- console.log('每秒执行一次');
- }, 1000);
- // 5秒后停止
- setTimeout(() => {
- clearInterval(intervalId);
- }, 5000);
2. Promises – 表示未来值
- // 创建Promise
- const myPromise = new Promise((resolve, reject) => {
- // 异步操作
- setTimeout(() => {
- const random = Math.random();
- if (random > 0.5) {
- resolve(`成功!随机数:${random}`);
- } else {
- reject(`失败!随机数:${random}`);
- }
- }, 1000);
- });
- // 使用Promise
- myPromise
- .then(result => {
- console.log('成功:', result);
- })
- .catch(error => {
- console.log('错误:', error);
- })
- .finally(() => {
- console.log('无论成功失败都会执行');
- });
- // Promise链式调用
- fetch('https://api.example.com/data')
- .then(response => response.json())
- .then(data => {
- console.log('数据:', data);
- return fetch('https://api.example.com/more-data');
- })
- .then(response => response.json())
- .then(moreData => {
- console.log('更多数据:', moreData);
- })
- .catch(error => {
- console.error('请求失败:', error);
- });
3. async/await – 处理promises的语法糖
- async function fetchData() {
- console.log("开始获取...");
- const response = await fetch("https://api.example.com");
- const data = await response.json();
- console.log("完成:", data);
- }
- fetchData();
- console.log("这个先运行");
详细示例和解释
1. 事件循环执行顺序示例
- console.log('1. 同步代码开始');
- setTimeout(() => {
- console.log('2. setTimeout 0ms');
- }, 0);
- setTimeout(() => {
- console.log('3. setTimeout 100ms');
- }, 100);
- Promise.resolve().then(() => {
- console.log('4. Promise微任务');
- });
- Promise.resolve().then(() => {
- console.log('5. 另一个Promise微任务');
- });
- console.log('6. 同步代码结束');
- // 输出顺序:
- // 1. 同步代码开始
- // 6. 同步代码结束
- // 4. Promise微任务
- // 5. 另一个Promise微任务
- // 2. setTimeout 0ms
- // 3. setTimeout 100ms
2. 微任务队列优先级
- console.log('开始');
- setTimeout(() => {
- console.log('setTimeout 1');
- Promise.resolve().then(() => {
- console.log('setTimeout 1 中的 Promise');
- });
- }, 0);
- setTimeout(() => {
- console.log('setTimeout 2');
- }, 0);
- Promise.resolve().then(() => {
- console.log('Promise 1');
- Promise.resolve().then(() => {
- console.log('Promise 1 中的 Promise');
- });
- });
- Promise.resolve().then(() => {
- console.log('Promise 2');
- });
- console.log('结束');
- // 输出顺序:
- // 开始
- // 结束
- // Promise 1
- // Promise 2
- // Promise 1 中的 Promise
- // setTimeout 1
- // setTimeout 1 中的 Promise
- // setTimeout 2
3. 实际应用示例
用户界面更新
- async function updateUserInterface() {
- console.log('开始更新UI');
- // 模拟API调用
- const userData = await fetchUserData();
- // 更新DOM(微任务)
- Promise.resolve().then(() => {
- updateUserProfile(userData);
- });
- // 显示加载状态
- showLoadingSpinner();
- // 延迟隐藏加载状态
- setTimeout(() => {
- hideLoadingSpinner();
- }, 1000);
- console.log('UI更新完成');
- }
- function fetchUserData() {
- return new Promise((resolve) => {
- setTimeout(() => {
- resolve({ name: '张三', email: 'zhangsan@example.com' });
- }, 2000);
- });
- }
- function updateUserProfile(userData) {
- console.log('更新用户资料:', userData);
- }
- function showLoadingSpinner() {
- console.log('显示加载动画');
- }
- function hideLoadingSpinner() {
- console.log('隐藏加载动画');
- }
- updateUserInterface();
错误处理
- async function handleUserAction() {
- try {
- console.log('开始用户操作');
- // 模拟多个异步操作
- const [userData, userPreferences] = await Promise.all([
- fetchUserData(),
- fetchUserPreferences()
- ]);
- // 处理数据
- const processedData = await processUserData(userData);
- // 保存结果
- await saveUserData(processedData);
- console.log('用户操作完成');
- } catch (error) {
- console.error('操作失败:', error);
- // 显示错误消息
- showErrorMessage(error.message);
- }
- }
- function fetchUserData() {
- return new Promise((resolve, reject) => {
- setTimeout(() => {
- if (Math.random() > 0.3) {
- resolve({ id: 1, name: '用户' });
- } else {
- reject(new Error('获取用户数据失败'));
- }
- }, 1000);
- });
- }
- function fetchUserPreferences() {
- return new Promise((resolve) => {
- setTimeout(() => {
- resolve({ theme: 'dark', language: 'zh' });
- }, 500);
- });
- }
- function processUserData(data) {
- return new Promise((resolve) => {
- setTimeout(() => {
- resolve({ ...data, processed: true });
- }, 300);
- });
- }
- function saveUserData(data) {
- return new Promise((resolve, reject) => {
- setTimeout(() => {
- if (Math.random() > 0.2) {
- resolve('保存成功');
- } else {
- reject(new Error('保存失败'));
- }
- }, 800);
- });
- }
- function showErrorMessage(message) {
- console.log('显示错误:', message);
- }
- handleUserAction();
4. 性能优化技巧
避免阻塞事件循环
- // 🚫 不良实践:阻塞事件循环
- function heavyComputation() {
- const start = Date.now();
- // 模拟重计算
- for (let i = 0; i < 1000000000; i++) {
- // 大量计算
- }
- console.log(`计算耗时:${Date.now() - start}ms`);
- }
- // ✅ 良好实践:使用setTimeout分割任务
- function nonBlockingComputation() {
- const start = Date.now();
- let i = 0;
- function processChunk() {
- const chunkSize = 1000000;
- const end = Math.min(i + chunkSize, 1000000000);
- for (; i < end; i++) {
- // 处理一小块数据
- }
- if (i < 1000000000) {
- // 还有更多工作要做,继续处理
- setTimeout(processChunk, 0);
- } else {
- // 完成
- console.log(`计算耗时:${Date.now() - start}ms`);
- }
- }
- processChunk();
- }
- // 使用Web Workers进行真正的并行处理
- function useWebWorker() {
- const worker = new Worker('worker.js');
- worker.postMessage({ type: 'heavyComputation' });
- worker.onmessage = function(event) {
- console.log('计算结果:', event.data);
- };
- }
批量DOM更新
- // 🚫 不良实践:频繁DOM更新
- function updateListBad(items) {
- items.forEach(item => {
- const element = document.createElement('div');
- element.textContent = item.name;
- document.body.appendChild(element);
- });
- }
- // ✅ 良好实践:批量更新
- function updateListGood(items) {
- // 使用DocumentFragment减少重排
- const fragment = document.createDocumentFragment();
- items.forEach(item => {
- const element = document.createElement('div');
- element.textContent = item.name;
- fragment.appendChild(element);
- });
- document.body.appendChild(fragment);
- }
- // 使用requestAnimationFrame优化动画
- function smoothAnimation() {
- let start = null;
- function animate(timestamp) {
- if (!start) start = timestamp;
- const progress = timestamp - start;
- // 更新动画
- const element = document.getElementById('animated');
- element.style.transform = `translateX(${progress / 10}px)`;
- if (progress < 1000) {
- requestAnimationFrame(animate);
- }
- }
- requestAnimationFrame(animate);
- }
常见陷阱和解决方案
1. 回调地狱
- // 🚫 回调地狱
- function getUserData(userId, callback) {
- fetchUser(userId, (user) => {
- fetchUserPosts(user.id, (posts) => {
- fetchUserComments(posts[0].id, (comments) => {
- callback({ user, posts, comments });
- });
- });
- });
- }
- // ✅ 使用Promise
- async function getUserData(userId) {
- const user = await fetchUser(userId);
- const posts = await fetchUserPosts(user.id);
- const comments = await fetchUserComments(posts[0].id);
- return { user, posts, comments };
- }
- // ✅ 使用Promise.all并行请求
- async function getUserDataParallel(userId) {
- const user = await fetchUser(userId);
- const [posts, comments] = await Promise.all([
- fetchUserPosts(user.id),
- fetchUserComments(user.id)
- ]);
- return { user, posts, comments };
- }
2. 错误处理
- // 🚫 忽略错误
- async function badErrorHandling() {
- const data = await fetchData();
- processData(data); // 如果fetchData失败,这里会抛出错误
- }
- // ✅ 正确错误处理
- async function goodErrorHandling() {
- try {
- const data = await fetchData();
- processData(data);
- } catch (error) {
- console.error('处理数据时出错:', error);
- showErrorMessage('操作失败,请重试');
- }
- }
- // ✅ 使用.catch()
- async function alternativeErrorHandling() {
- const data = await fetchData().catch(error => {
- console.error('获取数据失败:', error);
- return null;
- });
- if (data) {
- processData(data);
- }
- }
3. 内存泄漏
- // 🚫 可能导致内存泄漏
- function badTimer() {
- setInterval(() => {
- console.log('定时器运行');
- }, 1000);
- // 没有清理定时器
- }
- // ✅ 正确清理
- function goodTimer() {
- const intervalId = setInterval(() => {
- console.log('定时器运行');
- }, 1000);
- // 在组件卸载时清理
- return () => {
- clearInterval(intervalId);
- };
- }
- // 在React组件中使用
- function TimerComponent() {
- useEffect(() => {
- const intervalId = setInterval(() => {
- console.log('定时器运行');
- }, 1000);
- return () => clearInterval(intervalId);
- }, []);
- return <div>定时器组件div>;
- }
📌 总结
JavaScript通过事件循环实现非阻塞:异步操作通过Web APIs延迟执行,并通过任务队列和微任务队列排队。
Promise和async/await使异步代码更易读和管理:它们提供了更清晰的语法来处理异步操作。
微任务(Promises)总是在任务(setTimeout等)之前运行:这是事件循环的重要特性。
理解执行顺序对于调试异步代码至关重要:掌握事件循环的工作原理有助于避免常见的陷阱。
性能优化需要避免阻塞事件循环:使用适当的异步模式和工具来保持应用的响应性。
