事件循环与异步JavaScript

概述
JavaScript是单线程的,意味着它按单一序列执行代码。但多亏了事件循环和异步特性如setTimeout、Promises和async/await,它可以处理I/O操作、定时器和HTTP请求等任务而不会阻塞执行。
🔁 事件循环:核心概念
事件循环允许JavaScript通过将非阻塞操作放入队列并继续执行其他代码来执行非阻塞操作。当调用栈清空时,它检查队列并处理下一个任务。
🧠 涉及的组件
调用栈(Call Stack)
JavaScript代码执行的地方。它是LIFO(后进先出)。
Web APIs(由浏览器提供)
处理定时器、DOM事件、fetch请求等。
回调队列(任务队列)
存储要在栈清空后执行的回调函数。
微任务队列(Microtask Queue)
用于promises和queueMicrotask。比回调队列具有更高的优先级。
🔄 工作原理(简化流程)
  1. console.log("Start");

  2. setTimeout(() => {
  3. console.log("Timeout");
  4. }, 0);

  5. Promise.resolve().then(() => {
  6. console.log("Promise");
  7. });

  8. console.log("End");

  9. // 输出:
  10. // Start
  11. // End
  12. // Promise
  13. // Timeout
javascript
为什么?
console.log("Start") 同步 立即执行
setTimeout(...) 发送到Web APIs 延迟后将回调发送到任务队列
Promise.then(...) 微任务队列
console.log("End") 同步 执行
微任务队列运行:Promise
任务队列运行:Timeout
📘 异步JavaScript工具
1. setTimeout / setInterval 调度任务
  1. // 基本用法
  2. setTimeout(() => {
  3. console.log('3秒后执行');
  4. }, 3000);

  5. // 清除定时器
  6. const timerId = setTimeout(() => {
  7. console.log('这个不会执行');
  8. }, 5000);

  9. clearTimeout(timerId);

  10. // 间隔执行
  11. const intervalId = setInterval(() => {
  12. console.log('每秒执行一次');
  13. }, 1000);

  14. // 5秒后停止
  15. setTimeout(() => {
  16. clearInterval(intervalId);
  17. }, 5000);
javascript
2. Promises 表示未来值
  1. // 创建Promise
  2. const myPromise = new Promise((resolve, reject) => {
  3. // 异步操作
  4. setTimeout(() => {
  5. const random = Math.random();
  6. if (random > 0.5) {
  7. resolve(`成功!随机数:${random}`);
  8. } else {
  9. reject(`失败!随机数:${random}`);
  10. }
  11. }, 1000);
  12. });

  13. // 使用Promise
  14. myPromise
  15. .then(result => {
  16. console.log('成功:', result);
  17. })
  18. .catch(error => {
  19. console.log('错误:', error);
  20. })
  21. .finally(() => {
  22. console.log('无论成功失败都会执行');
  23. });

  24. // Promise链式调用
  25. fetch('https://api.example.com/data')
  26. .then(response => response.json())
  27. .then(data => {
  28. console.log('数据:', data);
  29. return fetch('https://api.example.com/more-data');
  30. })
  31. .then(response => response.json())
  32. .then(moreData => {
  33. console.log('更多数据:', moreData);
  34. })
  35. .catch(error => {
  36. console.error('请求失败:', error);
  37. });
javascript
3. async/await 处理promises的语法糖
  1. async function fetchData() {
  2. console.log("开始获取...");
  3. const response = await fetch("https://api.example.com");
  4. const data = await response.json();
  5. console.log("完成:", data);
  6. }

  7. fetchData();
  8. console.log("这个先运行");
javascript
详细示例和解释
1. 事件循环执行顺序示例
  1. console.log('1. 同步代码开始');

  2. setTimeout(() => {
  3. console.log('2. setTimeout 0ms');
  4. }, 0);

  5. setTimeout(() => {
  6. console.log('3. setTimeout 100ms');
  7. }, 100);

  8. Promise.resolve().then(() => {
  9. console.log('4. Promise微任务');
  10. });

  11. Promise.resolve().then(() => {
  12. console.log('5. 另一个Promise微任务');
  13. });

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

  15. // 输出顺序:
  16. // 1. 同步代码开始
  17. // 6. 同步代码结束
  18. // 4. Promise微任务
  19. // 5. 另一个Promise微任务
  20. // 2. setTimeout 0ms
  21. // 3. setTimeout 100ms
javascript
2. 微任务队列优先级
  1. console.log('开始');

  2. setTimeout(() => {
  3. console.log('setTimeout 1');
  4. Promise.resolve().then(() => {
  5. console.log('setTimeout 1 中的 Promise');
  6. });
  7. }, 0);

  8. setTimeout(() => {
  9. console.log('setTimeout 2');
  10. }, 0);

  11. Promise.resolve().then(() => {
  12. console.log('Promise 1');
  13. Promise.resolve().then(() => {
  14. console.log('Promise 1 中的 Promise');
  15. });
  16. });

  17. Promise.resolve().then(() => {
  18. console.log('Promise 2');
  19. });

  20. console.log('结束');

  21. // 输出顺序:
  22. // 开始
  23. // 结束
  24. // Promise 1
  25. // Promise 2
  26. // Promise 1 中的 Promise
  27. // setTimeout 1
  28. // setTimeout 1 中的 Promise
  29. // setTimeout 2
javascript
3. 实际应用示例
用户界面更新
  1. async function updateUserInterface() {
  2. console.log('开始更新UI');
  3. // 模拟API调用
  4. const userData = await fetchUserData();
  5. // 更新DOM(微任务)
  6. Promise.resolve().then(() => {
  7. updateUserProfile(userData);
  8. });
  9. // 显示加载状态
  10. showLoadingSpinner();
  11. // 延迟隐藏加载状态
  12. setTimeout(() => {
  13. hideLoadingSpinner();
  14. }, 1000);
  15. console.log('UI更新完成');
  16. }

  17. function fetchUserData() {
  18. return new Promise((resolve) => {
  19. setTimeout(() => {
  20. resolve({ name: '张三', email: 'zhangsan@example.com' });
  21. }, 2000);
  22. });
  23. }

  24. function updateUserProfile(userData) {
  25. console.log('更新用户资料:', userData);
  26. }

  27. function showLoadingSpinner() {
  28. console.log('显示加载动画');
  29. }

  30. function hideLoadingSpinner() {
  31. console.log('隐藏加载动画');
  32. }

  33. updateUserInterface();
javascript
错误处理
  1. async function handleUserAction() {
  2. try {
  3. console.log('开始用户操作');
  4. // 模拟多个异步操作
  5. const [userData, userPreferences] = await Promise.all([
  6. fetchUserData(),
  7. fetchUserPreferences()
  8. ]);
  9. // 处理数据
  10. const processedData = await processUserData(userData);
  11. // 保存结果
  12. await saveUserData(processedData);
  13. console.log('用户操作完成');
  14. } catch (error) {
  15. console.error('操作失败:', error);
  16. // 显示错误消息
  17. showErrorMessage(error.message);
  18. }
  19. }

  20. function fetchUserData() {
  21. return new Promise((resolve, reject) => {
  22. setTimeout(() => {
  23. if (Math.random() > 0.3) {
  24. resolve({ id: 1, name: '用户' });
  25. } else {
  26. reject(new Error('获取用户数据失败'));
  27. }
  28. }, 1000);
  29. });
  30. }

  31. function fetchUserPreferences() {
  32. return new Promise((resolve) => {
  33. setTimeout(() => {
  34. resolve({ theme: 'dark', language: 'zh' });
  35. }, 500);
  36. });
  37. }

  38. function processUserData(data) {
  39. return new Promise((resolve) => {
  40. setTimeout(() => {
  41. resolve({ ...data, processed: true });
  42. }, 300);
  43. });
  44. }

  45. function saveUserData(data) {
  46. return new Promise((resolve, reject) => {
  47. setTimeout(() => {
  48. if (Math.random() > 0.2) {
  49. resolve('保存成功');
  50. } else {
  51. reject(new Error('保存失败'));
  52. }
  53. }, 800);
  54. });
  55. }

  56. function showErrorMessage(message) {
  57. console.log('显示错误:', message);
  58. }

  59. handleUserAction();
javascript
4. 性能优化技巧
避免阻塞事件循环
  1. // 🚫 不良实践:阻塞事件循环
  2. function heavyComputation() {
  3. const start = Date.now();
  4. // 模拟重计算
  5. for (let i = 0; i < 1000000000; i++) {
  6. // 大量计算
  7. }
  8. console.log(`计算耗时:${Date.now() - start}ms`);
  9. }

  10. // ✅ 良好实践:使用setTimeout分割任务
  11. function nonBlockingComputation() {
  12. const start = Date.now();
  13. let i = 0;
  14. function processChunk() {
  15. const chunkSize = 1000000;
  16. const end = Math.min(i + chunkSize, 1000000000);
  17. for (; i < end; i++) {
  18. // 处理一小块数据
  19. }
  20. if (i < 1000000000) {
  21. // 还有更多工作要做,继续处理
  22. setTimeout(processChunk, 0);
  23. } else {
  24. // 完成
  25. console.log(`计算耗时:${Date.now() - start}ms`);
  26. }
  27. }
  28. processChunk();
  29. }

  30. // 使用Web Workers进行真正的并行处理
  31. function useWebWorker() {
  32. const worker = new Worker('worker.js');
  33. worker.postMessage({ type: 'heavyComputation' });
  34. worker.onmessage = function(event) {
  35. console.log('计算结果:', event.data);
  36. };
  37. }
javascript
批量DOM更新
  1. // 🚫 不良实践:频繁DOM更新
  2. function updateListBad(items) {
  3. items.forEach(item => {
  4. const element = document.createElement('div');
  5. element.textContent = item.name;
  6. document.body.appendChild(element);
  7. });
  8. }

  9. // ✅ 良好实践:批量更新
  10. function updateListGood(items) {
  11. // 使用DocumentFragment减少重排
  12. const fragment = document.createDocumentFragment();
  13. items.forEach(item => {
  14. const element = document.createElement('div');
  15. element.textContent = item.name;
  16. fragment.appendChild(element);
  17. });
  18. document.body.appendChild(fragment);
  19. }

  20. // 使用requestAnimationFrame优化动画
  21. function smoothAnimation() {
  22. let start = null;
  23. function animate(timestamp) {
  24. if (!start) start = timestamp;
  25. const progress = timestamp - start;
  26. // 更新动画
  27. const element = document.getElementById('animated');
  28. element.style.transform = `translateX(${progress / 10}px)`;
  29. if (progress < 1000) {
  30. requestAnimationFrame(animate);
  31. }
  32. }
  33. requestAnimationFrame(animate);
  34. }
javascript
常见陷阱和解决方案
1. 回调地狱
  1. // 🚫 回调地狱
  2. function getUserData(userId, callback) {
  3. fetchUser(userId, (user) => {
  4. fetchUserPosts(user.id, (posts) => {
  5. fetchUserComments(posts[0].id, (comments) => {
  6. callback({ user, posts, comments });
  7. });
  8. });
  9. });
  10. }

  11. // ✅ 使用Promise
  12. async function getUserData(userId) {
  13. const user = await fetchUser(userId);
  14. const posts = await fetchUserPosts(user.id);
  15. const comments = await fetchUserComments(posts[0].id);
  16. return { user, posts, comments };
  17. }

  18. // ✅ 使用Promise.all并行请求
  19. async function getUserDataParallel(userId) {
  20. const user = await fetchUser(userId);
  21. const [posts, comments] = await Promise.all([
  22. fetchUserPosts(user.id),
  23. fetchUserComments(user.id)
  24. ]);
  25. return { user, posts, comments };
  26. }
javascript
2. 错误处理
  1. // 🚫 忽略错误
  2. async function badErrorHandling() {
  3. const data = await fetchData();
  4. processData(data); // 如果fetchData失败,这里会抛出错误
  5. }

  6. // ✅ 正确错误处理
  7. async function goodErrorHandling() {
  8. try {
  9. const data = await fetchData();
  10. processData(data);
  11. } catch (error) {
  12. console.error('处理数据时出错:', error);
  13. showErrorMessage('操作失败,请重试');
  14. }
  15. }

  16. // ✅ 使用.catch()
  17. async function alternativeErrorHandling() {
  18. const data = await fetchData().catch(error => {
  19. console.error('获取数据失败:', error);
  20. return null;
  21. });
  22. if (data) {
  23. processData(data);
  24. }
  25. }
javascript
3. 内存泄漏
  1. // 🚫 可能导致内存泄漏
  2. function badTimer() {
  3. setInterval(() => {
  4. console.log('定时器运行');
  5. }, 1000);
  6. // 没有清理定时器
  7. }

  8. // ✅ 正确清理
  9. function goodTimer() {
  10. const intervalId = setInterval(() => {
  11. console.log('定时器运行');
  12. }, 1000);
  13. // 在组件卸载时清理
  14. return () => {
  15. clearInterval(intervalId);
  16. };
  17. }

  18. // 在React组件中使用
  19. function TimerComponent() {
  20. useEffect(() => {
  21. const intervalId = setInterval(() => {
  22. console.log('定时器运行');
  23. }, 1000);
  24. return () => clearInterval(intervalId);
  25. }, []);
  26. return <div>定时器组件div>;
  27. }
javascript
📌 总结
JavaScript通过事件循环实现非阻塞:异步操作通过Web APIs延迟执行,并通过任务队列和微任务队列排队。
Promise和async/await使异步代码更易读和管理:它们提供了更清晰的语法来处理异步操作。
微任务(Promises)总是在任务(setTimeout等)之前运行:这是事件循环的重要特性。
理解执行顺序对于调试异步代码至关重要:掌握事件循环的工作原理有助于避免常见的陷阱。
性能优化需要避免阻塞事件循环:使用适当的异步模式和工具来保持应用的响应性。