前端开发还没彻底弄懂事件循环?Low!

54 阅读4分钟

深入理解浏览器事件循环:前端开发的核心机制

如果你不了解事件循环,那么你就根本不懂前端!

浏览器的进程模型

何为进程?

程序运行需要专属的内存空间,我们可以将这块内存空间简单理解为进程

每个应用至少拥有一个进程,进程之间相互独立。即使需要进行通信,也必须经过双方同意才能实现。

何为线程?

有了进程之后,就可以运行程序的代码了。运行代码的"执行者"被称为线程

一个进程至少包含一个线程,因此在进程启动时会自动创建一个线程来运行代码,这个线程称为主线程

如果程序需要同时执行多段代码,主线程就会创建更多线程来分担任务。因此,一个进程中可以包含多个线程。

例如,一个游戏应用可能包含主线程、游戏逻辑线程、网络通信线程等。

浏览器的进程与线程架构

浏览器是一个典型的多进程、多线程应用程序。为了避免单个问题导致整个浏览器崩溃,浏览器启动时会创建多个进程。

您可以在浏览器的任务管理器中查看当前运行的所有进程。

其中最主要的进程包括:

  • 浏览器进程

    • 主要负责界面显示、用户交互和子进程管理
    • 浏览器进程内部会启动多个线程来处理不同的任务
  • 网络进程

    • 负责加载网络资源
    • 网络进程内部会启动多个线程来处理不同的网络任务
  • 渲染进程(与事件循环最相关)

    • 渲染进程启动后,会开启一个渲染主线程,负责执行HTML、CSS和JavaScript代码
    • 默认情况下,浏览器会为每个标签页开启一个新的渲染进程,以确保不同标签页之间互不干扰

渲染主线程的工作机制

渲染主线程需要处理众多任务,包括但不限于:

  1. 解析HTML
  2. 解析CSS
  3. 计算样式
  4. 布局计算
  5. 图层处理
  6. 每秒60次的页面更新
  7. 执行全局JavaScript和动画
  8. 处理事件处理函数
  9. 执行计时器回调函数
  10. ......

面对如此繁重的任务,主线程面临一个关键问题:如何高效调度这些任务?

实际场景中的挑战:

  • 当正在执行JavaScript函数时,用户点击了按钮,应该立即处理点击事件吗?
  • 当正在执行JavaScript函数时,某个计时器到期了,应该立即执行其回调吗?
  • 当浏览器进程通知"用户点击了按钮"的同时,某个计时器也到期了,应该优先处理哪一个?
  • ......

事件循环:智慧的解决方案

渲染主线程采用了一种巧妙的机制来处理这些挑战:任务队列

  1. 在初始化阶段,渲染主线程会进入一个无限循环
  2. 每次循环中,它会检查消息队列中是否有待处理的任务
  3. 如果有任务,则取出第一个任务执行,执行完毕后进入下一次循环
  4. 如果没有任务,则进入休眠状态
  5. 其他线程(包括其他进程的线程)可以随时向消息队列添加任务
  6. 新任务会被添加到队列末尾,添加时如果主线程处于休眠状态,则会被唤醒继续循环

这样,所有任务都能有条不紊地持续执行。这个完整的过程被称为事件循环

深入理解JavaScript的异步特性

为什么需要异步?

代码执行过程中,经常会遇到无法立即处理的任务,例如:

  • 计时器完成后需要执行的任务 —— setTimeoutsetInterval
  • 网络通信完成后需要执行的任务 —— XHRFetch
  • 用户操作后需要执行的任务 —— addEventListener

如果让渲染主线程等待这些任务完成,会导致主线程长期阻塞,进而造成浏览器卡顿甚至无响应。

JavaScript异步的本质

JavaScript是一门单线程语言,因为它运行在浏览器的渲染主线程中。每个标签页只有一个渲染主线程,它承担着页面渲染、执行JS和CSS等关键任务。

如果采用同步方式处理所有任务,极易导致主线程阻塞,使得消息队列中的其他任务无法执行。这不仅浪费主线程资源,还会导致页面无法更新,给用户造成卡死的体验。

浏览器的解决方案是采用异步机制

当遇到耗时任务(如定时器、网络请求等)时,主线程会将这些任务交给其他线程处理,自己立即转而执行其他任务。当其他线程完成任务后,会将预设的回调函数包装成任务,添加到消息队列末尾,等待主线程调度执行。

在这种异步模式下,浏览器能够实现永不阻塞的高效运行。