浏览器渲染机制和node事件循环

浏览器渲染机制

Document Object Model (DOM)

当浏览器读取 HTML 代码时,只要遇到 body、div 等 HTML 元素,就会创建一个名为 Node 的 JavaScript 对象。

浏览器从 HTML 文档中创建了 Node 之后,就要把这些节点对象创建成树状结构。

CSS Object Model (CSSOM)

是一种用于描述和操作CSS样式表的模型。它将CSS样式表表示为一个对象树,每个对象代表样式表中的一个规则或者样式信息。CSSOM 允许开发人员通过编程方式读取和操作CSS样式表,包括修改样式规则、查询元素的计算样式等。

在W3C标准中,它包含两个部分:描述样式表和规则等CSS的模型部分(CSSOM),和跟元素视图相关的View部分(CSSOM View)。

他提供了一些api包括:
规则部分,支持对元素属性的修改

document.styleSheets[0].insertRule("p { color:pink; }", 0)
document.styleSheets[0].removeRule(0)


视图部分
窗口 API

窗口API用于操作浏览器窗口的位置、尺寸等

  1. moveTo(x, y) 窗口移动到屏幕的特定坐标;
滚动 API

视口滚动API

scrollX 是视口的属性,表示X方向上的当前滚动距离

元素滚动API

scrollTop 元素的属性,表示Y方向上的当前滚动距离。

布局API

这是整个CSSOM中最常用到的部分,我们同样要分成全局API和元素上的API。

全局尺寸信息

如window.innerHeight, window.innerWidth 这两个属性表示视口的大小。

元素的布局信息

所以我们获取宽高的对象应该是“盒”,于是CSSOM View为Element类添加了两个方法:

  1. getClientRects();会返回一个列表,里面包含元素对应的每一个盒所占据的客户端矩形区域,这里每一个矩形区域可以用 x, y, width, height 来获取它的位置和尺寸。
  2. getBoundingClientRect()。

Render Tree

Render-Tree 也是一个将 DOM 树和 CSSOM 树组合在一起构建的树状结构。

渲染顺序

布局操作

首先浏览器创建每个单独的 Render-Tree 节点的布局。布局包括每个节点的大小(以像素为单位)和它将被打印在屏幕上的位置。这个过程被称为布局,因为浏览器正在计算每个节点的布局信息

绘制操作

由于 Render-Tree 中的元素(或子树)可以相互重叠,而且它们可以具有 CSS 属性,使它们经常改变外观、位置或几何显示(如动画),因此浏览器会为它创建一个图层

创建图层可以帮助浏览器在网页的整个生命周期中高效地执行绘制操作,比如在滚动或调整浏览器窗口大小的时候。拥有图层还可以帮助浏览器按照开发者的意图,正确地按照堆叠顺序(沿 z 轴)绘制元素。

现在我们有了图层,我们可以将它们组合起来,并在屏幕上绘制。但是浏览器并不是一次性绘制所有的图层。每个图层都是先单独绘制的

在每个图层里面,浏览器会对元素的任何可见属性,如边框、背景色、阴影、文字等进行单独的像素填充。

合成操作

我们拥有的是不同的图层(位图图像),它们应该按照特定的顺序绘制在屏幕上。在合成操作中,这些图层会被发送到 GPU 上,最终将其绘制在屏幕上。

每次回流(布局)或重绘时都要将整个图层送去绘制,这显然是很低效的。因此,一个图层被分解成不同的块,然后将其绘制在屏幕上

细节

细节一:script会阻塞dom的解析

如果它是一个嵌入式 script,那么它将首先执行该 script,然后继续解析 HTML,构建 DOM 树。所以所有的嵌入式 script都是解析器阻塞型的

如果 script 元素是外部 script 文件,浏览器会在主线程之外开始下载外部 script 文件,但在该文件下载完毕之前,会停止主线程的执行。这意味着在 script 文件下载之前,不会再进行 DOM 解析。

async 属性,当 DOM 解析器遇到一个带有 async 属性的外部 script 元素时,它不会在后台下载 script 文件时停止解析过程。但是一旦文件下载完毕,解析过程就会停止,script(代码)就会被执行。

defer 属性,它的工作原理与 async 属性类似,但与 async 属性不同的是,即使文件完全下载完毕,script 也不会执行。一旦解析器解析了所有的 HTML,也就是说 DOM 树已经完全构建完成,所有的 defer script就会被执行。与async 不同的是,所有的延迟 script 是按照它们在 HTML 文档(或 DOM 树)中出现的顺序来执行的。

细节二:css会阻塞dom的解析

DOM 和 CSSOM 树的构建都发生在主线程上,而且这些树的构建是同时进行的。它们共同构成了用于在屏幕上打印东西的 Render Tree,而 Render Tree 也随着 DOM 树的构建而逐步构建。

我们已经了解到,DOM 树的生成是增量的,这意味着当浏览器读取 HTML 时,它会将 DOM 元素添加到 DOM 树中。但 CSSOM 树却不是这样。与 DOM 树不同,CSSOM 树的构建不是递增的,必须以特定的方式进行

因此,如果浏览器在解析样式表内容时开始递增地构建 CSSOM,就会导致渲染树的多次渲染,因为样式覆盖规则会使同样的 CSSOM 节点,因后面新出现的样式表文件而导致更新。当 CSS 被解析时,可以在屏幕上看到元素样式的改变,这将是一种不愉快的用户体验。由于 CSS 样式是层叠的,一个规则的改变可能会影响许多元素。

因此,浏览器不会逐步处理外部 CSS 文件,CSSOM 树更新是在样式表中所有 CSS 规则处理完毕后一次性完成的。CSSOM 树更新完成后,再更新渲染树,然后渲染到屏幕上。

CSS 是一种渲染阻塞型资源。一旦浏览器提出获取外部样式表的请求,Render Tree 的构建就会停止。因此,关键渲染路径(CRP)也被卡住了,没有任何东西被渲染到屏幕上,如下图所示。然而,在后台下载样式表时,DOM 树的构建仍在进行中。

但是一旦样式表被下载和解析,导致 CSSOM 更新,我们的 JavaScript 现在有一个过时的元素的 CSS 值,因为新的 CSSOM 更新可能已经改变了该 DOM 元素的 CSS 属性。由于这个原因,在下载样式表的时候执行 JavaScript 是不安全的

浏览器会等到样式表被加载和解析。一旦样式表被解析,CSSOM 被更新,Render Tree 就会被更新,CRP 就会继续进行,从而使 Render Tree 绘制在屏幕上。由于这个原因,建议尽早加载所有外部样式表

node的事件循环

浏览器中的事件循环是根据 HTML 标准实现的,而 Node.js 中的事件循环则是基于 libuv 实现的。

libuv 是一个用 C 语言实现的高性能解决单线程非阻塞异步 I/O 的开源库,本质上它是对常见操作系统底层异步 I/O 操作的封装。

而在 nodejs 中,这都是 libuv 完成的。

几乎每个 Node API 都有异步执行版本libuv 直接负责它们的执行,libuv 会开启一个线程池,主线程执行到异步操作后,libuv 就会在线程池中调度空闲线程去执行,可以说 libuv 为 nodejs 提供了整个事件循环功能。

过程

与在浏览器中一样,在 nodejs 中 JS 最开始在主线程上执行,执行同步任务、发出异步请求、规划定时器生效时间、执行 process.nextTick 等,这时事件循环还没开始。

在上述过程中,如果没有异步操作,代码在执行完成后便直接退出。如果有,libuv 会把不同的异步任务分配给不同的线程,形成事件循环。在同步代码执行完后,nodejs 便会进入事件循环,依次执行不同队列中的任务。

Nodejs 事件循环中的消息队列共有 8 个,若引用之前宏队列、微队列的说法,具体可划分为:

  • 宏队列
    • timers (重要),计时器队列,负责处理 setTimeout 和 setInterval 定义的回调函数。
    • pending callback (待定回调)
      • 调用上一次事件循环没在 poll 阶段立刻执行,而延迟的 I/O 回调函数
    • idle prepare (空闲准备)
      • 仅供 nodejs 内部使用
    • poll (重要),轮询队列,该阶段会处理除 timers 和 check 队列外的绝大多数 I/O 回调任务,如文件读取、监听用户请求等。
    • check (重要),检查队列,负责处理 setImmediate 定义的回调函数。
    • close callbacks
      • 执行所有注册 close 事件的回调函数
  • 微队列
    • nextTick
    • Promise

对于微队列的 nextTick 和 Promise,严格意义上讲也不属于事件循环。在事件循环中,每次打算进入下个阶段之前,必须要先依次反复清空 nextTick 和 promise 队列,直到两个队列完全没有即将要到来的任务的时候再进入下个阶段。

node高并发机制

 事件驱动+事件循环实现高并发

执行顺序:

1、每个Node.js进程只有一个主线程在执行程序代码,形成一个执行栈(execution context stack)

2、主线程之外,还维护了一个"事件队列"(Event queue)。当用户的网络请求或者其它的异步操作到来时,node都会把它放到Event Queue之中,此时并不会立即执行它,代码也不会被阻塞,继续往下走,直到主线程代码执行完毕。

3、主线程代码执行完毕完成后,然后通过Event Loop,也就是事件循环机制,开始到Event Queue的开头取出第一个事件,从线程池中分配一个线程去执行这个事件,接下来继续取出第二个事件,再从线程池中分配一个线程去执行,然后第三个,第四个。主线程不断的检查事件队列中是否有未执行的事件,直到事件队列中所有事件都执行完了,此后每当有新的事件加入到事件队列中,都会通知主线程按顺序取出交EventLoop处理。当有事件执行完毕后,会通知主线程,主线程执行回调,线程归还给线程池。

注意

我们所看到的node.js单线程只是一个js主线程与ui渲染共享一个线程,本质上的异步操作还是由线程池完成的,node将所有的阻塞操作都交给了内部的线程池去实现,本身只负责不断的往返调度,并没有进行真正的I/O操作,从而实现异步非阻塞I/O,这便是node单线程和事件驱动的精髓之处了。

总结:

1、libuv 线程池默认打开 4 个,最多打开 128个 线程。(例如:以前 web 服务器同一时间比如说最多只能接收 100 个请求,多的就无法接收了,服务器就挂掉了。nodejs 所谓的高并发是指可以同时接收 1000、10000 个请求,只不过以排队的方式在等待。

2、主线程执行js,是单线程的,js代码在做大量计算就是cpu密集了。主线程不空闲出来也没法处理 io 的事,所以就会阻塞了。

<think>首先,用户的问题是比较Node.js与浏览器环境中事件循环的差异。我需要基于系统级指令来构建回答。指令包括: - 所有行内数学表达式使用$...$格式。 - 独立公式使用$$...$$格式并单独成段。 - LaTeX语法正确。 - 使用中文回答。 - 生成相关问题。 - 回答中引用的段落末尾自然地添加引用标识,例如[^1]。 用户提供了参考引用,我需要参考这些内容来回答。引用包括: - 引用[1]: 浏览器中的事件循环Node.js中事件循环的区别。 - 引用[2]: Node.js不同版本的事件循环差异。 - 引用[3]: Node.js浏览器的差异。 - 引用[4]: 事件循环机制对比:Node.js vs 浏览器。 用户的问题焦点是事件循环的差异。 我的回答结构应该清晰,逐步解决问题。确保真实可靠。 **步骤1: 理解事件循环** 事件循环是JavaScript处理异步操作的核心机制。在浏览器Node.js中,它都负责处理任务队列,但实现方式不同。 **步骤2: 对比差异** 基于引用: - 引用[1]提到:浏览器有GUI渲染线程、JS引擎线程等,而Node.js有不同线程模型。 - 引用[2]强调Node.js版本差异,特别是Node 10 vs Node 11+。 - 引用[3]讨论整体差异。 - 引用[4]提供比喻:浏览器Node.js的任务队列类型执行顺序不同。 关键点: - **浏览器事件循环**: 基于HTML规范,有宏任务(如setTimeout、事件监听)微任务(如Promise)。微任务在宏任务之间执行。 - **Node.js事件循环**: 基于libuv,有多个阶段:timers、pending callbacks、idle prepare、poll、check、close callbacks。微任务处理时机不同,特别是process.nextTick优先级最高。 - **版本差异**: Node.js 10及以下,微任务在宏任务阶段之间执行;Node.js 11+,微任务在宏任务之后立即执行,更接近浏览器行为。 - **线程模型**: 浏览器有多个线程(如渲染线程),而Node.js是单线程但支持异步I/O。 **步骤3: 构建回答结构** 1. **介绍事件循环**: 简要解释什么是事件循环。 2. **浏览器事件循环**: 描述其机制。 3. **Node.js事件循环**: 描述其机制,包括阶段。 4. **关键差异**: 对比任务队列、执行顺序、微任务处理等。 5. **版本影响**: 提到Node.js版本差异。 6. **总结**: 简要总结差异。 7. **生成相关问题**: 添加相关问题部分。 **步骤4: 使用LaTeX** 在回答中,如果有数学表达式,使用$...$或$$...$$。但事件循环涉及较少数学,所以可能不需要。如果提到优先级或队列,可以用简单表示,如$priority$。 **步骤5: 引用** 在回答中,当引用信息时,添加引用标识[^1], [^2]等。 **最终回答框架** - **标题或开头**: 直接回答问题。 - **主体**: - 什么是事件循环? - 浏览器事件循环机制。 - Node.js事件循环机制。 - 主要区别。 - Node.js版本差异。 - **结尾**: 总结。 - **相关问题**: 添加相关问题部分。 确保语言为中文。</think>### Node.js与浏览器事件循环的差异比较 事件循环是JavaScript处理异步操作的核心机制,它在浏览器Node.js环境中都扮演着关键角色。然而,两者的实现方式存在显著差异,这源于它们的设计目标:浏览器专注于用户界面交互,而Node.js侧重于服务器端I/O密集型任务。以下我将逐步解释这些差异,确保内容基于可靠的技术规范(如HTML标准libuv库实现),并参考权威资料[^1][^2][^3][^4]。 #### 1. **事件循环的基本概念** 事件循环是一种任务调度机制,用于管理异步代码的执行。它维护任务队列,循环处理队列中的任务。在JavaScript中,任务分为宏任务(如setTimeout、I/O操作)微任务(如Promise回调)。两者的核心区别在于任务队列的结构执行顺序。 - **宏任务(Macrotask)**: 代表较大的异步操作单元,例如$setTimeout()$或用户事件。 - **微任务(Microtask)**: 代表较小的、高优先级的异步操作,例如$Promise.then()$,它们通常在当前宏任务结束后立即执行[^1][^4]。 #### 2. **浏览器事件循环机制** 在浏览器环境中,事件循环基于HTML规范实现,主要涉及多个线程协同工作: - **线程模型**: - JS引擎线程:执行JavaScript代码(单线程)。 - GUI渲染线程:负责页面渲染,与JS线程互斥。 - 异步线程:包括定时器线程、事件触发线程HTTP请求线程,它们将任务推入队列[^1][^3]。 - **事件循环阶段**: 1. 执行一个宏任务(如script标签代码)。 2. 执行所有微任务(清空微任务队列)。 3. 更新渲染(如有需要)。 4. 重复循环,处理下一个宏任务。 - **关键特点**: - 微任务优先级高:在宏任务结束后,所有微任务会立即执行,形成“插队”效果。例如: ```javascript setTimeout(() => console.log('宏任务'), 0); // 宏任务 Promise.resolve().then(() => console.log('微任务')); // 微任务 // 输出顺序: 微任务 -> 宏任务 ``` - 任务队列简单:只有宏任务队列微任务队列两种类型[^1][^4]。 #### 3. **Node.js事件循环机制** Node.js使用libuv库实现事件循环,其设计更注重非阻塞I/O操作。事件循环分为多个阶段,每个阶段处理特定类型的任务: - **事件循环阶段(按顺序)**: 1. **Timers**: 处理$setTimeout()$$setInterval()$回调。 2. **Pending callbacks**: 执行系统操作(如TCP错误)的回调。 3. **Idle, Prepare**: 内部使用,通常不涉及应用代码。 4. **Poll**: 检索新I/O事件(如文件读写或网络请求)。 5. **Check**: 处理$setImmediate()$回调。 6. **Close callbacks**: 执行关闭事件(如socket.on('close'))[^2][^4]。 - **微任务处理**: - 在每个阶段结束后,Node.js会检查微任务队列。 - **高优先级微任务**: $process.nextTick()$拥有最高优先级,它会在当前操作后立即执行(甚至优先于Promise)。例如: ```javascript setTimeout(() => console.log('宏任务'), 0); Promise.resolve().then(() => console.log('微任务')); process.nextTick(() => console.log('nextTick')); // 输出顺序: nextTick -> 微任务 -> 宏任务 ``` - **版本差异**: - Node.js 10及以下:微任务在宏任务阶段之间执行,可能导致延迟。 - Node.js 11+:微任务在每个宏任务后立即执行,行为更接近浏览器(但$process.nextTick()$仍优先)[^2][^4]。 #### 4. **主要差异总结** 以下是浏览器Node.js事件循环的关键区别点: | 特性 | 浏览器事件循环 | Node.js事件循环 | 参考来源 | |-----------------------|----------------------------------------|------------------------------------------|----------| | **任务队列类型** | 简单队列:宏任务微任务 | 多阶段队列:Timers、Poll、Check等 | [^1][^4] | | **微任务执行时机** | 在宏任务结束后立即执行所有微任务 | 在每个事件循环阶段结束后执行微任务 | [^2][^4] | | **高优先级机制** | 微任务(如Promise)优先于宏任务 | $process.nextTick()$优先级最高,其次是微任务 | [^4] | | **线程模型** | 多线程(JS线程、渲染线程等)协同 | 单线程(主事件循环),但异步I/O使用线程池 | [^1][^3] | | **I/O处理** | 有限I/O支持(如XMLHttpRequest) | 优化I/O密集型操作(文件、网络) | [^3] | | **版本影响** | 行为相对稳定 | Node.js 11+后微任务调度更接近浏览器 | [^2] | **比喻解释**:引用[^4]将事件循环比作快餐店。在浏览器中,宏任务是普通顾客,微任务是VIP(优先插队);在Node.js中,宏任务分时段顾客(不同阶段),微任务是超级VIP($process.nextTick()$最优先)。 #### 5. **实际影响示例** 考虑以下代码在不同环境下的输出: ```javascript console.log('开始'); setTimeout(() => console.log('setTimeout'), 0); Promise.resolve().then(() => console.log('Promise')); process.nextTick(() => console.log('nextTick')); // Node.js特有 // 浏览器输出: 开始 -> Promise -> setTimeout // Node.js输出: 开始 -> nextTick -> Promise -> setTimeout ``` - **浏览器**: 微任务(Promise)在宏任务(setTimeout)前执行。 - **Node.js**: $process.nextTick()$优先,然后是微任务[^2][^4]。 #### 6. **总结** Node.js与浏览器事件循环主要差异在于任务队列结构、微任务执行时机优先级机制浏览器采用简单的双队列模型,微任务插队执行;Node.js使用多阶段循环,$process.nextTick()$提供最高优先级,且版本升级(如Node 11+)缩小了与浏览器的差距。理解这些差异有助于优化异步代码性能,避免跨环境问题[^1][^2][^3][^4]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值