Eventloop

JavaScript 的事件循环(Event Loop)是其异步编程的核心机制,它决定了代码的执行顺序。以下是事件循环的核心原理和工作流程:


一、核心组成

  1. 调用栈(Call Stack)
  • 用于执行同步代码的栈结构(后进先出)。

  • 遇到函数调用时压入栈顶,执行完毕后弹出。

  1. 任务队列(Task Queues)
  • 宏任务队列(MacroTask Queue)

    • setTimeout, setInterval, DOM 事件回调, I/O 操作等。
  • 微任务队列(MicroTask Queue)

    • Promise.then/catch/finally, queueMicrotask, MutationObserver
  1. 事件循环线程(Event Loop Thread)
  • 持续检查调用栈是否为空,调度队列中的任务。

二、事件循环工作流程

  1. 执行同步代码
  • 同步代码直接进入调用栈,逐行执行。
  1. 处理异步任务
  • 遇到异步操作(如 setTimeout)时:

    • 浏览器将回调注册到 Web API 环境,等待触发条件(如定时器到期)。

    • 条件满足后,回调被推入对应的任务队列。

  1. 事件循环调度
  • 当调用栈为空时

    1. 优先清空微任务队列(全部执行完毕)。

    2. 执行一个宏任务

    3. 再次清空微任务队列。

    4. 重复此过程。


三、执行顺序示例


console.log("1"); // 同步代码

setTimeout(() => console.log("2"), 0); // 宏任务

Promise.resolve().then(() => {

  console.log("3"); // 微任务

  setTimeout(() => console.log("4"), 0); // 嵌套宏任务

});

console.log("5"); // 同步代码

输出顺序1 → 5 → 3 → 2 → 4

  • 解析

    1. 同步代码输出 15

    2. 清空微任务队列,输出 3,同时将 setTimeout(4) 加入宏任务队列。

    3. 执行宏任务队列,先输出 2,再执行下一个宏任务输出 4


四、关键特性

  1. 微任务优先级高于宏任务
  • 每次调用栈清空后,必须执行完所有微任务才会处理宏任务。
  1. 渲染时机
  • 在微任务执行完毕后、下一个宏任务执行前,浏览器可能执行渲染(如 requestAnimationFrame)。
  1. Node.js 差异
  • Node.js 的事件循环分为多个阶段(如 timerspollcheck),微任务在阶段切换间处理。

五、常见问题

  1. 为什么 Promise.thensetTimeout 先执行?
  • 微任务队列优先级高,会在当前宏任务结束后立即执行。
  1. 如何避免阻塞事件循环?
  • 避免长时间同步代码(如复杂计算),拆分任务或用 Web Worker
  1. 死循环风险
  • 微任务中递归添加微任务会导致事件循环永远无法处理宏任务。

六、调试技巧

  • 使用浏览器开发者工具的 Sources 面板 查看调用栈和任务队列。

  • 使用 console.trace() 追踪函数调用路径。


Web Worker

Web Worker 是浏览器提供的一种技术,允许 JavaScript 在后台线程中运行代码,从而避免阻塞主线程(UI 线程),实现多线程并行处理。它是解决 JavaScript 单线程限制的核心方案之一,尤其适合处理计算密集型任务或耗时操作。


一、核心特性

  1. 独立线程

    • Web Worker 运行在独立的全局上下文中,与主线程隔离。
    • 无法直接操作 DOM(避免线程冲突)。
    • 通过消息机制与主线程通信postMessageonmessage)。
  2. 线程类型

    • 专用 Worker(Dedicated Worker)
      • 仅能被创建它的脚本使用,生命周期与创建者绑定。
    • 共享 Worker(Shared Worker)
      • 可被多个脚本共享(需同源),通过端口(port)通信。
    • Service Worker
      • 用于离线缓存、网络代理等(属于 PWA 核心技术)。
  3. 浏览器支持

    • 现代浏览器全面支持(包括移动端),IE 10+ 部分支持。

二、使用场景

  1. 计算密集型任务

    • 图像/视频处理(如 Canvas 像素计算)。
    • 大数据分析(如排序、加密解密)。
    • 复杂数学运算(如物理模拟、3D 渲染)。
  2. 避免 UI 阻塞

    • 文件读写(如大文件解析)。
    • 长时间轮询或网络请求。
  3. 后台任务

    • 日志上报、数据预加载、定时任务。

三、基本用法

1. 创建专用 Worker

// 主线程代码(main.js)
const worker = new Worker('worker.js');

// 向 Worker 发送数据
worker.postMessage({ type: 'calc', data: 1000 });

// 接收 Worker 返回的数据
worker.onmessage = (e) => {
  console.log('Result:', e.data);
};

2. Worker 线程代码(worker.js)

// 监听主线程消息
self.onmessage = (e) => {
  if (e.data.type === 'calc') {
    const result = heavyCalculation(e.data.data);
    self.postMessage(result); // 返回结果
  }
};

function heavyCalculation(n) {
  // 模拟耗时计算(如斐波那契数列)
  let sum = 0;
  for (let i = 0; i < n; i++) sum += i;
  return sum;
}

四、通信机制

  1. 数据传输方式

    • 结构化克隆算法:自动深拷贝对象(支持大多数数据类型,如 ArrayBuffer)。
    • Transferable Objects:零拷贝转移所有权(如 postMessage(buffer, [buffer]))。
  2. 错误处理

    • 通过 worker.onerror 捕获 Worker 内部错误。

五、限制与注意事项

  1. 无法访问的 API

    • DOM、windowdocumentparent 对象。
    • alert()confirm() 等 UI 相关方法。
  2. 作用域隔离

    • Worker 内部使用 self 代替全局对象(而非 window)。
  3. 资源限制

    • 浏览器可能限制同时创建的 Worker 数量。
    • 大量 Worker 会占用内存和 CPU。

六、优化技巧

  1. 复用 Worker

    • 避免频繁创建/销毁,通过消息复用实例。
  2. 任务拆分

    • 将大任务拆分为子任务分批次处理。
  3. 终止 Worker

    • 使用 worker.terminate() 及时释放资源。

七、替代方案

  1. WebAssembly

    • 处理更复杂的计算,性能接近原生代码。
  2. 分时函数(setTimeout 分块)

    • 将任务拆分为小块通过事件循环穿插执行(兼容性更好,但效率较低)。

总结

  • 适用场景:需要并行处理、避免主线程阻塞的任务。
  • 核心优势:提升页面响应速度,优化用户体验。
  • 典型应用:在线编辑器、数据可视化、游戏引擎等。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容