深入理解浏览器事件循环:前端开发的核心机制
如果你不了解事件循环,那么你就根本不懂前端!
浏览器的进程模型
何为进程?
程序运行需要专属的内存空间,我们可以将这块内存空间简单理解为进程。
每个应用至少拥有一个进程,进程之间相互独立。即使需要进行通信,也必须经过双方同意才能实现。
何为线程?
有了进程之后,就可以运行程序的代码了。运行代码的"执行者"被称为线程。
一个进程至少包含一个线程,因此在进程启动时会自动创建一个线程来运行代码,这个线程称为主线程。
如果程序需要同时执行多段代码,主线程就会创建更多线程来分担任务。因此,一个进程中可以包含多个线程。
例如,一个游戏应用可能包含主线程、游戏逻辑线程、网络通信线程等。
浏览器的进程与线程架构
浏览器是一个典型的多进程、多线程应用程序。为了避免单个问题导致整个浏览器崩溃,浏览器启动时会创建多个进程。
您可以在浏览器的任务管理器中查看当前运行的所有进程。
其中最主要的进程包括:
-
浏览器进程
- 主要负责界面显示、用户交互和子进程管理
- 浏览器进程内部会启动多个线程来处理不同的任务
-
网络进程
- 负责加载网络资源
- 网络进程内部会启动多个线程来处理不同的网络任务
-
渲染进程(与事件循环最相关)
- 渲染进程启动后,会开启一个渲染主线程,负责执行HTML、CSS和JavaScript代码
- 默认情况下,浏览器会为每个标签页开启一个新的渲染进程,以确保不同标签页之间互不干扰
渲染主线程的工作机制
渲染主线程需要处理众多任务,包括但不限于:
- 解析HTML
- 解析CSS
- 计算样式
- 布局计算
- 图层处理
- 每秒60次的页面更新
- 执行全局JavaScript和动画
- 处理事件处理函数
- 执行计时器回调函数
- ......
面对如此繁重的任务,主线程面临一个关键问题:如何高效调度这些任务?
实际场景中的挑战:
- 当正在执行JavaScript函数时,用户点击了按钮,应该立即处理点击事件吗?
- 当正在执行JavaScript函数时,某个计时器到期了,应该立即执行其回调吗?
- 当浏览器进程通知"用户点击了按钮"的同时,某个计时器也到期了,应该优先处理哪一个?
- ......
事件循环:智慧的解决方案
渲染主线程采用了一种巧妙的机制来处理这些挑战:任务队列。
- 在初始化阶段,渲染主线程会进入一个无限循环
- 每次循环中,它会检查消息队列中是否有待处理的任务
- 如果有任务,则取出第一个任务执行,执行完毕后进入下一次循环
- 如果没有任务,则进入休眠状态
- 其他线程(包括其他进程的线程)可以随时向消息队列添加任务
- 新任务会被添加到队列末尾,添加时如果主线程处于休眠状态,则会被唤醒继续循环
这样,所有任务都能有条不紊地持续执行。这个完整的过程被称为事件循环。
深入理解JavaScript的异步特性
为什么需要异步?
代码执行过程中,经常会遇到无法立即处理的任务,例如:
- 计时器完成后需要执行的任务 ——
setTimeout、setInterval - 网络通信完成后需要执行的任务 ——
XHR、Fetch - 用户操作后需要执行的任务 ——
addEventListener
如果让渲染主线程等待这些任务完成,会导致主线程长期阻塞,进而造成浏览器卡顿甚至无响应。
JavaScript异步的本质
JavaScript是一门单线程语言,因为它运行在浏览器的渲染主线程中。每个标签页只有一个渲染主线程,它承担着页面渲染、执行JS和CSS等关键任务。
如果采用同步方式处理所有任务,极易导致主线程阻塞,使得消息队列中的其他任务无法执行。这不仅浪费主线程资源,还会导致页面无法更新,给用户造成卡死的体验。
浏览器的解决方案是采用异步机制:
当遇到耗时任务(如定时器、网络请求等)时,主线程会将这些任务交给其他线程处理,自己立即转而执行其他任务。当其他线程完成任务后,会将预设的回调函数包装成任务,添加到消息队列末尾,等待主线程调度执行。
在这种异步模式下,浏览器能够实现永不阻塞的高效运行。