# 事件循环 **Repository Path**: zeng_yu_jie/event-loop ## Basic Information - **Project Name**: 事件循环 - **Description**: 事件循环 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 1 - **Created**: 2025-06-06 - **Last Updated**: 2026-01-13 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 事件循环 ## 浏览器进程模型 ### 什么是进程? ``` 程序运行需要有它自己专属的内存空间,可以把这一块内存空间简单理解为进程 每个应用至少有一个进程,进程之间是相互独立的,即使要通信,也需要双方同意 ``` ### 什么是线程? ``` 有了进程之后,就可以运行程序的代码了 运行代码的 “人” 称为 “线程” 一个进程至少有一个线程(必须要有人运行代码,否则会被系统杀死),所以在进程开启后会自动创建一个线程来运行代码,该线程称之为主线程 如果程序需要同时执行多块代码,主线程就会启动更多的线程来执行代码,所以一个进程中可以包含多个线程 如果主线程结束,那么整个程序就结束了 ``` ### 浏览器有哪些进程和线程 ``` 浏览器是一个多进程的应用程序 浏览器内部工作极其复杂 为了避免互相影响,为了减少连环崩溃的几率,当启动浏览器后,他会自动开启多个进程 可以在任务管理器中查看当前的所有进程 其中,最主要的进程有: 1.浏览器进程: 主要负责界面显示(不是网页的展示,是浏览器中标签页,前进后退刷新按钮,导航栏等页面的展示),用户交互(用户在窗口的点击,滚动等),子进程管理等,浏览器进程内部会启动多个线程处理不用的任务 2.网络进程 负责加载网络资源,网络进程内部会启动多个线程来处理不同的网络任务 3.渲染进程 渲染进程启动后,会开启一个渲染主线程,主线程负责执行html,css,js代码 默认情况下,浏览器会为每个标签页开启一个新的渲染进程,以保证不同标签页之间不相互影响 ``` ### 渲染主线程是如何工作的? ``` 渲染主线程是浏览器中最繁忙的线程,需要它处理的任务包括但不限于: 1.解析 HTML 2.解析 CSS 3.计算样式 4.布局 5.处理图层 6.每秒把页面画60次 7.执行全局JS代码 8.执行事件处理函数 9.执行计时器的回调函数 ... ``` #### 为什么渲染进程不适用多个线程来处理这些事情? ``` ``` #### 要处理这么多任务,主线程遇到一个前所未有的难度:如何调度任务? ``` 比如: -我正在执行一个JS函数,执行到一半的时候用户点击了按钮,我该立刻去执行点击事件的处理函数吗? -我正在执行一个JS函数,执行到一半的时候某个计时器到达了时间,我该立刻去执行他的回调吗 -浏览器进程通知我“用户点击了按钮”,与此同时,某个计时器也到达了时间,我应该处理哪一个呢? ... 渲染主线程想到一个绝妙的主意来处理这个问题:排队! ``` ![alt](./resource/image1.png) ``` 1.最开始的时候,渲染主线程会进入一个无限循环 2.每一次循环会检查消息队列中是否有任务存在,如果有,就取第一个任务执行,执行完之后进入下一个循环;如果没有,则进入休眠状态 3.其他所有线程(包括其他进程的线程)可以随时向消息队列中添加任务(例如浏览器的进程中的线程监听了鼠标点击事件后要执行对应的回调函数),新任务会加到消息队列的末尾 在添加新任务时,如果主线程是休眠状态,则会将其唤醒以继续循环拿取任务 这样一来,就可以让每个任务都有条不紊的,持续的进程下去了 整个过程被称做事件循环(消息循环) ``` ## 何为异步? ``` 代码在执行过程中,会遇到一些无法立即处理的任务,比如: -计时完成后需要执行的任务 --setTimeout setInterval -网络通信完成后需要执行的任务 --XHR Fetch -用户操作后需要执行的任务 --addEventListnner 如果让渲染主线程等待这些任务的时机达到,就会导致主线程长期处于[阻塞]的状态,从而导致浏览器[卡死],以下这些任务在同步过程的一个流程图 ``` ![alt](./resource/image2.png) ``` 渲染主线程承担极其重要的工作,无论如何都不能阻塞 因此浏览器选择异步来解决这个问题 以下是异步过程的一个流程图 ``` ![alt](./resource/image3.png) ``` 使用异步的方式,渲染主线程永不阻塞 ``` ### 如何理解JS的异步? ``` JS是一门单线程语言,这是因为它运行在浏览器的渲染主线程当中,而渲染主线程只有一个,渲染主线程承担着许多工作,渲染页面,执行JS都在其中运行 如果使用同步的方式,就极有可能(需要涉及到特殊任务)导致主线程阻塞,从而导致消息队列中很多其他任务无法得到执行,这样一来,一方面会导致繁忙的主线程白白消耗时间,另一方面导致页面无法及时更新,给用户造成卡死现象 所以浏览器采用异步的方式来避免.具体的做法是: 执行全局JS代码时,当某些任务发生时,比如:计时器,网络,事件监听,主线程将任务交给其他线程去处理(异步任务的"处理"可以在其他线程中进行,比如计时器线程,网络线程) 自身立即结束该任务的执行,转而执行后续的代码,当其他线程完成时,将事先传递的回调函数包装成任务,加入到消息队列的末尾排队,等待主线程调度执行 在这种异步模式下,浏览器永不阻塞,从而最大限度的保证了单线程的流程运行 ``` ### JS为何会阻碍渲染? ``` 具体查看代码index01.html ``` ### 任务有优先级吗? ``` 任务没有优先级,在消息队列中先进先出 但是消息队列有优先级: -根据W3C的解释: 抛弃了以前宏队列和微队列的说法 1.每个任务都有一个任务类型,同一个类型的任务必须在一个队列,不同类型的任务可以分属于不同的队列,在一次事件循环中浏览器可以根据实际情况从不同的队列中取出任务执行 2.浏览器必须准备好一个微队列,微队列中的任务优先所有任务执行 -目前的chrome的实现中,至少包含了下面的队列: 1.延时队列:用于存放计时器到达后的回调任务(中) 2.交互队列:用于存放用户操作后产生的事件处理任务(高) 3.微队列:用户存放需要最快执行的任务(最高) ``` ![alt](./resource/image4.png) ### 添加任务到微队列的主要方式 ``` -Promise -MutationObserver 例如:Promise.resolve().then(函数) ``` ## JS的计时器能做到精确计时吗,为什么? ``` -计算机硬件没有原子钟,是用cpu寄存器做的,无法做到精确计时 -操作系统的计时函数本身有偏差,js代码调用的是操作系统的计时函数,会导致偏差 -受事件循环的影响,计时器的回调函数只能在主线程空闲时运行,会带来偏差 ``` # 浏览器渲染原理 ## 什么叫渲染 ``` 渲染 render 在浏览器中,渲染是指将html字符串变成屏幕上每个的像素点的信息 Vue和React的render函数也是渲染,他的渲染含义是得到一个虚拟dom ``` ## 渲染的时间点 ``` 浏览器中有一个网络进程,网络进程中会开一个线程通信获取HTML资源 拿到HTML之后会生成一个渲染任务丢到浏览器的消息队列中,在事件循环机制的作用下,渲染主线程会取出消息队列中的渲染任务,开启渲染流程 ``` ## 渲染流程 ``` 渲染第一步: 解析HTML 解析过程中遇到CSS解析CSS,遇到JS执行JS,为了提高解析效率.浏览器在开启解析前,会启动一个预解析的线程 率先下载HTML中外部CSS文件和外部JS文件(不会处理HTML文件中的行内样式,行内样式由渲染主线程直接处理) 如果主线程解析link位置.此时外部的CSS文件还没有下载解析好,主线程不会等待,继续解析后续的HTML 这是因为下载和解析CSS的工作是在预解析线程中进行的,这就是CSS不会阻塞HTML的根本原因 在解析HTML中遇到行内样式时,会将其收集起来,并不会立即构建CSSOM树 如果主线程解析到script位置,会停止解析HTML,转而等待JS文件下载好,并将全局代码解析执行完成后,才会继续解析HTML 这是因为JS代码的执行过程可能会修改当前的DOM树,所以DOM树的生成必须暂停,这就是JS会阻塞HTML的根本原因 第一步完成后,会得到DOM树和CSSOM树,浏览器的默认样式,内部样式,外部样式,行内样式均会包含在CSSOM树中 渲染第二步: 样式计算 主线程会遍历得到的DOM树,依次为树中的每个节点计算出它最终的样式,称之为Computed Style 在这一过程中,很多预设值会变成绝对值,比如red会变成rgb(255,0,0),相对单位会变成绝对单位,比如rem会变成px 渲染第三步 布局Layout 页面上元素之间的位置和尺寸是相互影响的,例如:包含块,子元素的width:100% 是相对与包含块 布局阶段会依次遍历DOM树的每个节点,计算每个节点的几何信息,例如节点的宽高,相对包含块的位置 DOM树和Layout树不一定是一一对应的,因为有些DOM元素可能是隐藏的(dispaly:none),不会出现在布局树中 在DOM树中有些元素的样式可能有::before,这个样式生成的元素不会在DOM树中出现,但是会出现在布局树中,因为它有单独的几何信息 渲染第四步 分层layer 主线程会使用一套复杂的策略对整个布局树中进行分层 分层的好处在于,将来某一个层改变后,仅会对该层进行后续处理,从而提升效率 滚动条,堆叠上下文,transform,opacity等样式都会或多或少的影响分层的结果 渲染第五步: 绘制Paint 这里的绘制,是为每一层生成如何绘制的指令 渲染主线程的工作到此为止,剩余步骤交给其他线程完成 渲染第六步 分块-Tiling 分块的工作是交给多个线程同时进行的 分块将每一层分为多个小块,让多个线程同时处理大面积的页面渲染 分块后,浏览器可以只处理用户当前可见区域附近的块 当用户滚动页面时,只需要处理新进入视口的块,而不是整个层 渲染第七步 光栅化-Raser 光栅化就是将每个块变成位图 优先处理靠近视口的块,浏览器可以只处理用户当前可见区域附近的块来提高效率 渲染第八步 画-Draw 合成线程计算出每个位图在屏幕中的位置,交给GPU进行最终的呈现 ``` ![alt](./resource/image5.png) ## 浏览器输入地址后按下回车之后到页面呈现发生了什么 ``` 网络:拿到HTML 渲染:渲染 ``` ## HTML解析过程中遇到JS代码怎么办 ``` 渲染主线程遇到JS时必须暂停一切行为,等待下载并执行后才能继续,预解析线程可以分担一些下载JS的任务 因为JS中可能会操作DOM元素更改之前解析过的东西 ``` ## 什么是reflow? ``` 当我们做了一些操作dom或cssom时,这些操作影响到节点的几何信息的时候会(layout) 重新布局到重新画出页面的过程就叫做reflow reflow的本质就是重新计算layout树 为了避免连续的多次操作导致布局树反复计算,浏览器会合并这些操作,当JS代码全部完成后在进行统一计算,所以改动属性造成的reflow是异步完成的 也同样因为如此,当JS获取布局属性时,就可能无法获取到最新的布局信息 浏览器在反复权衡下,最终决定:在获取属性时立即reflow,其他时候等待所有JS代码全部完成后在reflow ``` ## 为什么transform效率高? ``` 具体看index02.html代码 ``` ![alt](./resource/image6.png) ![alt](./resource/image7.png)