浏览器渲染原理深度解析(浏览器的事件循环机制)(三)

浏览器渲染原理深度解析(浏览器的事件循环机制)(三)

浏览器的进程模型
图片[1] - 浏览器渲染原理深度解析(浏览器的事件循环机制)(三) - 宋马
最主要的进程

1.浏览器进程
主要负责界⾯显示、⽤户交互、⼦进程管理等。浏览器进程内部会启动多个
线程处理不同的任务。
2. ⽹络进程
负责加载⽹络资源。⽹络进程内部会启动多个线程来处理不同的⽹络任务。
3.渲染进程(本节课重点讲解的进程)
渲染进程启动后,会开启⼀个渲染主线程,主线程负责执⾏
HTML

CSS


JS
代码。
默认情况下,浏览器会为每个标签⻚开启⼀个新的渲染进程,以保证不同的
标签⻚之间不相互影响。

渲染主线程是如何⼯作的

在最开始的时候,渲染主线程会进⼊⼀个⽆限循环每⼀次循环会检查消息队列中是否有任务存在。如果有,就取出第⼀个任务 执⾏,执⾏完⼀个后进⼊下⼀次循环;如果没有,则进⼊休眠状态。其他所有线程(包括其他进程的线程)可以随时向消息队列添加任务。新任 务会加到消息队列的末尾。在添加新任务时,如果主线程是休眠状态,则会
将其唤醒以继续循环拿取任务

这样⼀来,就可以让每个任务有条不紊的、持续的进⾏下去了。
整个过程,被称之为事件循环(消息循环)

浏览器的一帧内发生了什么
这个问题是对前一个问题(渲染管线)的完美进阶,它将我们的视角从“绘制过程”本身,拉高到了“时间调度”的层面。如果你能把
渲染管线

事件循环
(Event Loop)在一帧的尺度内结合起来,你就真正掌握了浏览器渲染性能优化的脉络

1. 概念引入:为何要关心“一帧”?
为了让用户感觉流畅,浏览器需要以屏幕的刷新率(通常是
60Hz
,即每秒60次)来更新画面。
1000ms / 60 ≈ 16.67ms
这就是我们常说的“
黄金16.67ms预算
”。浏览器必须在这极短的时间内,完成从接收任务到最终在屏幕上绘制出新画面的所有工作。如果任何一帧的工作超过了这个时间,下一帧就无法按时到来,用户就会感觉到卡顿(Jank)或掉帧。
因此,关心“一帧”内发生了什么,本质上是在关心:我们如何成为一个合格的“时间管理者”,确保我们写的代码(JS)、样式(CSS)和页面结构(HTML)能够在16.67ms的预算内,与浏览器自身的任务和谐共处,共同完成一帧的渲染。

2.深度剖析:一帧的生命周期 & 各路神仙的排队顺序
表达方式:”一个宏任务,所有微任务,然后UI渲染,最后看闲不闲“ (值得注意的是,这个模型是简化的,用于帮助理解。实际的浏览器环境比Node.js环境更复杂,渲染相关的任务调度可能因浏览器厂商和具体场景而异,但核心优先级顺序是稳定的)

事件循环(Event Loop)

持续检查调用栈是否为空若调用栈为空,优先从微任务队列取出所有任务执行微任务队列清空后,从宏任务队列取出一个任务执行。重复上述过程,形成循环

阶段一:处理宏任务 (Macrotask / Task)
这是谁
setTimeout
,
setInterval
,
IO操作
,
用户输入事件
(click, scroll等)。
它在做什么? 事件循环从宏任务队列中取出一个最老的任务来执行。在一帧的开始,通常只执行一个宏任务。

阶段二:清空微任务队列 (Microtask Queue)
这是谁
Promise.then/catch/finally
,
MutationObserver
,
queueMicrotask()

它在做什么? 在上一个宏任务执行完毕后,浏览器会立即检查微任务队列,并循环执行其中的所有任务,直到队列被清空为止。

阶段三:UI渲染的决策与执行 (UI Rendering)
这是谁?
requestAnimationFrame(rAF)
以及后面的 Style, Layout 等渲染管线阶段。
它在做什么? 判断是否需要渲染: 浏览器评估自上一帧是否有视觉变化。无变化则可能跳过渲染。
执行 rAF 回调: 如果需要渲染,执行所有 requestAnimationFrame 注册的回调。这是动画更新的最佳时机。
执行渲染管线: 紧接着 rAF 回调执行完毕,浏览器会依次执行
Style(样式计算)
->
Layout(布局)
->
Paint(绘制)
->
Composite(合成)

阶段四:空闲时间调度 (Idle Time)
这是谁? requestIdleCallback(rIC)。
它在做什么? 在一帧的所有工作都完成之后,如果距离16.67ms的截止时间还有剩余,浏览器就认为现在是“空闲”的。此时,它会去调用 requestIdleCallback 注册的回调函数。
浏览器兼容性提示:
requestIdleCallback
目前在 Safari 浏览器中支持尚不完整,需要注意兼容性或使用 polyfill。

优先级总结 (简洁复述/背诵版)
在一帧的生命周期内,不同任务的执行优先级顺序如下:

宏任务(Macrotask): 从队列中取一个执行。
微任务(Microtask): 执行上一个宏任务产生的所有微任务,直到队列清空。
requestAnimationFrame(rAF): 在下一次重绘之前执行。是所有UI更新和动画的最高优先级。
UI 渲染(Layout, Paint, etc.): 在rAF回调执行完毕后,执行页面渲染。
requestIdleCallback(rIC): 在一帧的末尾,只有在浏览器有空闲时间时才执行,优先级最低。

核心关系:宏任务 -> 微任务 -> rAF -> 渲染 -> rIC

面试题及解析

Q1: Vue 的 nextTick 和 React 的 setState 是如何利用事件循环机制来实现异步更新的?结合 React 18
谈谈你的理解。

解析:
这个问题将框架原理与底层事件循环联系在一起,是考察候选人知识深度的绝佳问题

回答思路:

核心目的: 无论是
nextTick
还是
setState
,它们的核心目的都是异步更新,即将多次连续的数据变更操作合并为一次,这是一种批处理(
Batching
)思想,旨在避免不必要的渲染。
实现机制 – 优先级选择: 为了尽快地在“本次事件循环”内,但在“浏览器渲染”前完成DOM更新,微任务是最佳选择。
Vue nextTick 的实现

Vue 3 的 nextTick 优先使用
Promise.resolve().then()
,这创建了一个微任务。
Vue 2 中,为了兼容性,它有一个优雅降级的策略:Promise -> MutationObserver (微任务) -> setTimeout(fn, 0) (宏任务)

React setState 的演进:

React 17及之前: 在
React
可以控制的执行上下文中(如React事件处理器),setState是异步批量执行的,通过微任务触发更新。但在React无法控制的上下文中(如 setTimeout, 原生DOM事件监听器内),setState会表现为同步的。

React 18及之后 (并发特性): React 18 引入了自动批量更新(
Automatic Batching
)。现在,无论在何处调用
setState
(包括
setTimeout
或原生事件监听器),默认都会被自动批量处理,统一在微任务中异步执行。这使得行为更加一致和可预测。

React 18 的新工具: React 18 提供了
startTransition

useDeferredValue
等并发工具,可以标记某些更新为“非紧急”,让React在更合适的时机(比如浏览器空闲时)执行它们,从而优化用户体验。这与 flushSync 强制同步更新形成了鲜明对比。
总结: 现代前端框架普遍倾向于利用微任务实现高效的异步更新。React 18通过自动批量更新统一了这一行为,并提供了并发工具进行更精细的渲染控制。

文章引用
https://juejin.cn/post/7537983271393722431#heading-1

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容