浏览器事件循环
本文探讨的是浏览器的事件循环,如果你对此颇有心得,欢迎指出本文的错误。如果你学过 NodeJs 的事件循环,请暂时忘记,因为浏览器的事件循环和 NodeJs 的事件循环完全不同。
浏览器的进程模型
浏览器是一个多进程多线程的应用程序,内部工作极其复杂(复杂度接近操作系统)。
当我们打开浏览器时,它会自动启动多个进程。使用 Shift + Esc
打开浏览器任务管理器即可查看,其中有:
-
浏览器进程
负责界面显示、用户交互、子进程管理,同时提供存储等功能。浏览器进程内部会启动多个线程来处理不同的任务。
界面显示是指除标签页以外的界面,包括地址栏、书签栏、前进后退按钮等,这些界面在每个 Tab 页中都是类似的,所以在浏览器进程中统一维护。
-
GPU 进程
负责 3D 绘制等。
-
网络进程
负责网络资源的加载。网络进程内部会启动多个线程来处理不同的网络任务。
-
插件进程
负责插件的运行。毕竟插件也可能崩溃,所以需要单独的进程来与渲染进程隔离开来。
-
渲染进程
默认情况下一个 Tab 标签页就对应着一个渲染进程,负责页面渲染、脚本执行、事件处理等。由于进程之间是相互隔离的,所以一个页面的崩溃不会影响其他页面。
渲染进程内部会有多个线程来处理不同的任务,其中最主要的是渲染主线程
还有一种模式是一个站点对应一个渲染进程,这种模式下,一个站点的所有页面都在同一个渲染进程中。从而减少了进程的数量。
渲染主线程
渲染主线程是渲染进程的主线程,它处理的任务有:
- 解析 HTML
- 解析 CSS
- 计算样式
- 布局
- 处理图层
- 绘制页面(如每秒绘制 60 次页面)
- 执行 JS 代码
- 执行事件处理函数
- 执行定时器回调函数
- ······
要处理这么多的任务,渲染主线程如何进行任务的调度呢?比如下面问题:
- 我正在执行一个 JS 代码,突然有一个事件触发了,我应该先处理哪个?
- 我正在执行一个 JS 代码,突然有一个定时器到期了,我应该先处理哪个?
- 用户点击了按钮,同时有一个定时器到期了,我应该先处理哪个?
渲染主线程想出一个绝妙的主意来处理这个问题:排队
- 在最开始的时候,渲染主线程会进入一个无限循环
- 每次循环会检测消息队列是否有任务存在。如果有,则取出第一个任务执行,执行完进行下一个循环;如果没有,则进入休眠状态。
- 其他所有线程(包括其他进程的线程)可以随时向消息队列中添加任务。新任务会加入到消息队列的末尾。在添加新任务时,如果渲染主线程正在休眠,则会被唤醒,继续循环拿取任务。
整个过程,称之为事件循环(消息循环)。
JS 单线程 & 非阻塞
JS 是一门单线程的语言。 JS 运行在浏览器的渲染主线程中(暂时不考虑 NodeJS),而渲染主线程是一个单线程。
在 JS 刚发明的时候,将 JS 设计成单线程可以简化开发,避免多线程的复杂性。如果 JS 是多线程的,那么两个 JS 线程同时操作 DOM,一个线程删除一个节点,另一个线程又在这个节点上添加一个节点,这样就会出现问题。
随着技术的发展,人们也认识到 JS 单线程的局限性,虽然单线程可以保证程序的执行顺序,但是也限制了程序的执行效率。现在虽然有了 Web Worker,但是 Web Worker 也只是辅助线程,并不能操作 DOM。因此 JS 依然是一门单线程的语言。
JS 是一门非阻塞的语言
非阻塞指