浏览器网络、Socket、Event Loop、Renderer、Blink、V8 学习笔记
这篇笔记用于串联浏览器的多进程架构、网络 I/O、事件循环,以及一次 fetch 从页面发起、到服务器处理、再到页面更新的完整过程。
本文主要使用 Chromium 的架构帮助理解,但需要注意:
HTML、Fetch、Event Loop 等 Web 标准通常只规定可观察行为,不规定浏览器内部必须使用哪个进程或线程实现。
因此,本文中的进程、线程、队列和通信链路是便于理解的 Chromium 风格模型,不应被理解为所有浏览器都必须采用的固定实现。
核心结论
浏览器是由多个进程、线程、任务队列和运行时组件协作运行的系统,V8 只是其中负责 JavaScript 的一部分。
- Browser Process:负责浏览器外壳、标签页管理、导航协调、权限和进程管理等工作。
- Network Service:负责 DNS、代理、连接管理、HTTP、TLS、缓存等网络能力;现代 Chromium 中它可能运行在独立进程中,而不只是 Browser Process 内的一条固定“网络线程”。
- Renderer Process:承载网页内容,通常包含 Blink、V8、渲染主线程、合成线程等组件。
- Blink:Chromium 的渲染引擎,负责 DOM、HTML/CSS 解析、Web API、样式、布局、事件分发以及与页面事件循环的协作。
- V8:JavaScript 引擎,负责解析、编译和执行 JavaScript,并实现 ECMAScript 语言能力。
- 操作系统:实现 TCP/IP 协议栈,通过 Socket API 向浏览器和服务器程序提供网络能力。
- 服务器程序:长期运行并监听端口;网络库从 Socket 中读取字节,HTTP 实现将字节解析成请求,Web 框架再匹配并调用处理函数。
- Node.js 服务器:单个进程通常在一条主 JavaScript 线程上执行用户代码;同步代码会阻塞该线程,异步 I/O 等待期间则可以处理其他就绪任务。
最重要的认识是:
浏览器拥有 JavaScript 引擎,而不是 JavaScript 引擎拥有浏览器。
V8 可以近似理解成浏览器内部的 JavaScript 执行器,但它不是完整的浏览器运行时,也不负责网络、DOM、CSS、布局或页面事件循环的整体调度。
1. 浏览器整体架构
现代浏览器通常采用多进程、多线程架构。以 Chromium 为例,可以先用下面的简化模型理解:
1 | Chrome / Browser |
| 组件 | 主要职责 |
|---|---|
| Browser Process | 管理浏览器外壳、导航、标签页、权限和其他进程 |
| Network Service | 负责网络请求、协议处理、连接管理和缓存等能力 |
| Renderer Process | 承载一个或多个页面或 frame 的执行与渲染环境 |
| Blink | 实现 DOM、HTML/CSS、Web API、布局、事件分发等 Web 平台能力 |
| V8 | 实现 ECMAScript,解析、编译并执行 JavaScript |
| Renderer Main Thread | 运行大量页面主线程工作,包括 JS、DOM、样式、布局和事件处理 |
| Compositor Thread | 处理图层合成、滚动等部分可以脱离主线程完成的工作 |
| GPU Process | 负责与 GPU 交互及图形相关任务 |
一个标签页不一定严格对应一个 Renderer Process。Chrome 会根据站点隔离、跨站 iframe、进程复用、扩展和安全策略决定进程分配。
1.1 Blink 与 V8 的关系
Blink 和 V8 不是简单的上下级关系,而是两个相互调用、共同运行在 Renderer 中的组件。
当 JavaScript 调用 Web API 时:
1 | JavaScript |
当浏览器需要执行 JavaScript 时:
1 | Blink / 页面调度系统 |
例如:
1 | document.querySelector("#app"); |
其中:
- 函数调用表达式由 V8 执行。
document和 DOM 查询能力由 Blink 实现。- V8 通过绑定层调用 Blink。
- Blink 查询 DOM 后把结果转换为 JavaScript 可访问的对象。
因此:
V8 负责执行 JavaScript;Blink 负责大量浏览器和 Web 平台能力。两者通过绑定层协作。
2. TCP、Socket API、HTTP 与操作系统
需要区分协议、操作系统接口和应用程序库:
1 | 业务代码 / Web 框架 |
2.1 IP 与 TCP
IP 负责寻址和尽力将数据包送往目标主机,但不保证数据一定到达,也不保证到达顺序。
TCP 建立在 IP 之上,为两个网络端点提供可靠、有序的字节流。
操作系统内核中的 TCP 实现通常负责:
- TCP 连接建立与关闭
- ACK、超时与重传
- 数据乱序重排
- 重复数据处理
- 滑动窗口与流量控制
- 拥塞控制
- 发送与接收缓冲区管理
一条 TCP 连接通常可由四元组区分:
1 | 源 IP、源端口、目标 IP、目标端口 |
因此,多个客户端可以同时连接同一个服务器端口,操作系统仍能区分每一条连接。
2.2 Socket 与 Socket API
Socket 可以理解为操作系统维护的通信端点及其相关状态。
Socket API 是应用程序操作这些通信端点的接口。常见操作包括:
1 | socket() 创建 Socket |
应用调用 send() 时,并不是 Socket API 自己完成物理传输,而是将数据交给内核网络协议栈。
应用调用 recv() 时,通常是从内核为对应 Socket 维护的接收缓冲区中读取字节。
Socket API 不只支持 TCP,也可用于 UDP、Unix Domain Socket 等。Windows 常见 Winsock,Linux 和 Unix 系统常见 BSD Socket 风格接口。
2.3 HTTP 与 HTTP 实现
HTTP 是应用层协议,它规定:
- 请求方法
- URL 与路径
- 请求头和响应头
- 请求体和响应体
- 状态码
- 消息分帧和连接语义
HTTP 协议本身是一组规则,不是一段会主动运行的代码。
真正处理 HTTP 的是浏览器网络栈、服务器网络库或 HTTP 库。
1 | 发送: |
对于 TCP 来说,下面的 HTTP/1.1 请求只是一串需要可靠、有序传输的字节:
1 | GET /users/42 |
准确说法是:
操作系统实现 TCP/IP 协议栈,Socket API 将网络能力暴露给应用;浏览器或服务器的 HTTP 实现按照协议组织和解析数据,并调用底层网络能力。
HTTP/1.1 和 HTTP/2 通常基于 TCP;HTTPS 在 HTTP 与 TCP 之间使用 TLS;HTTP/3 基于 QUIC,而 QUIC 通常运行在 UDP 之上。
3. Network Service 如何等待网络事件
“Network Thread”适合作为教学上的简化概念,但不应理解为浏览器中必然只有一条、名字固定的网络线程。
现代浏览器的网络实现可能包含:
- Network Service 进程
- I/O 线程
- Socket 监听机制
- DNS、代理和证书相关线程
- IPC 消息循环
- 定时器与任务队列
在 Linux 风格的简化模型中,某条负责 I/O 的线程可以近似理解为:
1 | while (running) { |
底层等待机制可能使用:
- Linux:
epoll - macOS / BSD:
kqueue - Windows:IOCP
- 其他平台专用机制
当没有就绪事件时,负责等待的线程可以进入休眠,不持续占用 CPU。此时:
- Renderer Main Thread 仍可以运行。
- Browser Process 的 UI 线程仍可以运行。
- 其他线程和进程仍可以运行。
- 操作系统和网卡仍可以继续接收数据。
3.1 epoll_wait() 的概念模型
在 Linux 中,可以使用下面的模型理解:
1 | I/O 线程调用 epoll_wait() |
当某个受监听的文件描述符就绪时:
1 | Socket 或唤醒 fd 变为 ready |
代码从阻塞调用返回后继续向下执行:
1 | events = epoll_wait(epollFd, ...); |
3.2 任务通知和网络数据
I/O 线程通常需要处理两类来源。
控制任务或跨线程消息
例如 Renderer 发起一次请求后,需要通过 IPC 或其他任务投递机制通知 Network Service:
1 | Renderer Process |
底层可能使用管道、事件对象、消息端口、平台唤醒机制等。
eventfd 或所谓 wakeupFd 可以作为帮助理解的例子,但不是 Fetch 标准要求,也不是所有平台和 Chromium 版本都必须采用的固定实现。
Socket 就绪事件
当服务器响应数据到达后:
1 | 服务器 |
随后网络实现从 Socket 中读取数据,继续完成 TLS、HTTP 和响应体处理。
4. 一次 fetch 的网络链路
4.1 页面发起请求
页面执行:
1 | const promise = fetch("/api/user"); |
可以按下面的模型理解:
1 | Renderer Main Thread |
需要注意:
fetch()不是 V8 自己实现的网络功能。
V8 只负责执行函数调用。真正的 Fetch API、请求对象、CORS 检查协调、响应对象和流接口属于浏览器提供的 Web 平台能力。
4.2 创建或复用连接
Network Service 收到请求后,可能:
- 命中缓存而不进行真实网络传输
- 复用已有 HTTP/1.1 Keep-Alive 连接
- 复用已有 HTTP/2 或 HTTP/3 连接
- 创建新的 TCP/TLS 或 QUIC 连接
- 跟随重定向
- 经过代理服务器
- 执行证书验证
- 因 Service Worker 而走其他处理路径
因此,不能简单地认为每次 fetch() 都会创建一个新 Socket。
简化流程如下:
1 | 收到请求任务 |
4.3 接收响应
服务器响应数据到达后:
1 | 服务器响应 |
这里需要特别修正:
fetch()返回的 Promise 通常在响应状态和响应头可用、并创建了Response对象后兑现,不需要等待整个响应体全部下载完毕。
因此:
1 | const response = await fetch("/large-file"); |
执行到下一行时,响应体可能仍在通过网络持续到达。
真正等待完整响应体的是:
1 | const data = await response.json(); |
或者:
1 | const text = await response.text(); |
它们需要消费响应体流,通常要等到对应内容读取和解析完成。
完整模型:
1 | 响应头可用 |
5. 浏览器中的事件循环
浏览器页面的事件循环不是 V8 单独拥有的。
更准确地说:
HTML 标准定义了事件循环、任务、微任务和任务源等概念;浏览器运行时负责实现这些调度规则,V8 负责执行其中涉及的 JavaScript。
在 Chromium 中,Blink、Renderer 的消息循环、调度器以及 V8 共同完成这些行为。
不要简单记成:
1 | Event Loop 属于 V8 |
更适合的模型是:
1 | 浏览器页面运行时 |
5.1 页面事件循环的简化模型
1 | while (running) { |
这只是概念模型,不是浏览器源码的逐行实现。
每轮可以近似理解为:
1 | 选择并执行一个 Task |
需要注意:
- 浏览器不保证每执行一个 Task 都一定渲染一次。
- 渲染通常与刷新时机、页面状态和浏览器调度策略有关。
- 不同任务来自不同任务源,不能总是假设存在一条全局、严格 FIFO 的“宏任务队列”。
5.2 V8 的角色
当浏览器选择了一个包含 JavaScript 回调的任务时:
1 | 页面调度系统选择 Task |
因此,可以把 V8 近似理解为:
JavaScript 代码执行器。
但完整地说,V8 还负责:
- JavaScript 源码解析
- 字节码生成
- JIT 编译与优化
- JavaScript 调用栈
- 对象和内存管理
- 垃圾回收
- ECMAScript Promise 等语言级能力
- 微任务执行所需的 JavaScript 引擎机制
V8 不负责:
- TCP、HTTP 或 DNS
- HTML 和 CSS 解析
- DOM 的底层实现
- 页面 Layout 和 Paint
- SSE 协议解析
- 浏览器整体的页面事件循环调度
6. Task 与 Microtask
6.1 Task
Task 常被中文资料称为“宏任务”,不过 HTML 标准主要使用 task 这个术语。
常见来源包括:
- 初始脚本执行
setTimeout和setInterval- 用户输入事件
postMessageMessageChannel- SSE 的事件分发
- 某些网络状态事件
- DOM 事件分发
- 历史记录和导航相关工作
任务可以来自不同任务源。浏览器根据标准规则和调度策略选择可运行任务。
6.2 Microtask
常见来源包括:
Promise.thenPromise.catchPromise.finallyawait后续queueMicrotaskMutationObserver
到达微任务检查点时,浏览器会持续执行微任务,直到当前微任务队列为空。
如果微任务执行期间继续加入新的微任务,新微任务通常也会在同一个检查点继续执行。
1 | console.log(1); |
执行过程:
- 当前 Script Task 开始执行。
- 输出
1。 - 注册定时器;回调将在条件满足后成为 Task。
- 为
Promise.then注册 reaction;已兑现 Promise 使其成为微任务。 - 输出
4。 - 当前 Script Task 中的 JavaScript 执行完成。
- 执行微任务检查点,输出
3。 - 后续事件循环中执行定时器 Task,输出
2。
最终输出:
1 | 1 |
6.3 微任务不是只存在于“JS Task”中
更准确地说:
微任务检查点与事件循环和 JavaScript 执行环境有关,不是某种名叫“JS Task”的任务内部专属机制。
常见情况下,在一个任务执行完成、JavaScript 执行栈退出后,会进行微任务检查点。
此外,规范还可能在其他明确规定的时机执行微任务检查点。
因此,适合记忆的模型是:
1 | 执行一段当前任务 |
7. HTML Parser、Blink 与 V8
Renderer 收到 HTML 响应体数据后,Blink 的 HTML Parser 可以边接收边解析:
1 | HTML 响应体字节持续到达 |
浏览器不必等整个 HTML 文件下载完成后才开始解析。
7.1 遇到普通同步脚本
例如:
1 | <script> |
简化流程:
1 | HTML Parser 解析文档 |
这里要注意:
- Blink 负责 HTML Parser 和 DOM。
- V8 负责执行脚本。
- 脚本可以通过 DOM API 反过来修改 Blink 管理的 DOM。
- 外部脚本下载通常可由网络系统进行,但经典同步脚本可能阻塞后续解析和执行。
7.2 defer 和 async
普通外部脚本:
1 | <script src="app.js"></script> |
通常会阻塞 HTML Parser,直到脚本可执行并完成。
defer:
1 | <script defer src="app.js"></script> |
通常并行下载,在文档解析完成后、DOMContentLoaded 之前按文档顺序执行。
async:
1 | <script async src="app.js"></script> |
通常并行下载,下载完成后尽快执行,不保证与其他异步脚本保持文档顺序,并可能中断解析。
8. 如何理解异步
异步不等于一定新建线程。
更准确地说:
异步表示当前操作可以先发起或登记后续逻辑,在结果尚未准备好时不持续占用当前 JavaScript 调用栈;结果可用后,再由运行时按相应规则安排后续逻辑。
通用模型:
1 | 当前 JavaScript 发起异步操作 |
对于 fetch:
1 | V8 执行 fetch() |
如果随后调用:
1 | await response.json(); |
则还会继续:
1 | 持续消费响应体流 |
9. 多个网络请求为什么不会互相卡住
1 | fetch("/api/a"); |
V8 只是依次执行两次 fetch() 调用。每次调用都会快速返回一个 Promise,网络工作被交给浏览器网络系统。
因此当前同步脚本可以继续:
1 | 发起请求 A |
网络系统可以同时管理大量请求和连接状态:
1 | 请求 A:等待 DNS / 连接 / 响应 |
底层不会以“同步等待 A 完成后再看 B”的方式管理所有请求。
此外,多个请求不一定对应多个 TCP Socket:
- HTTP/1.1 可能使用多个连接,也可能复用 Keep-Alive 连接。
- HTTP/2 可以在一条连接上并发传输多个 Stream。
- HTTP/3 可以在一条 QUIC 连接中并发传输多个 Stream。
- 请求也可能命中缓存而不访问服务器。
10. fetch、流式响应、SSE 与 WebSocket
| 方式 | 通信方向 | 连接与数据特点 | 常见场景 |
|---|---|---|---|
普通 fetch |
请求—响应 | 一次请求对应一次响应;响应体本身仍是 Stream | 普通 API 请求 |
| Fetch Stream | 以响应数据为主 | JavaScript 主动读取响应体流 | 流式文本、大文件、AI 输出 |
| SSE | 服务端到客户端 | 一个长期不结束的 HTTP 响应,以文本事件格式持续发送 | 通知、日志、进度、内容流 |
| WebSocket | 双向 | 建立 WebSocket 会话后以 Frame 双向传输 | 聊天、协作、实时控制 |
10.1 普通 fetch
1 | fetch("/api/user").then(async (response) => { |
简化链路:
1 | V8 执行 fetch() |
10.2 Fetch Stream
1 | const response = await fetch("/api/chat"); |
简化处理过程:
1 | Socket 收到一部分响应体数据 |
这里的 value 通常是字节数据,如 Uint8Array,还可能需要使用 TextDecoder 进行文本解码。
必须牢记:
网络分块、HTTP 分块、ReadableStream 每次读到的数据和业务消息边界不一定一致。
一次 reader.read() 可能得到:
- 半条业务消息
- 一条完整业务消息
- 多条业务消息拼在一起
因此,应用层通常需要自己维护缓冲区并识别业务协议边界。
10.3 SSE
1 | const source = new EventSource("/events"); |
SSE 的本质是:
一个
Content-Type: text/event-stream的长期 HTTP 响应。
服务器持续在同一个响应体中写入文本:
1 | event: progress |
处理链路可以理解为:
1 | 服务器持续写响应体 |
需要特别注意:
- SSE Parser 不是 V8。
- V8 不解析
data:、event:、id:或retry:。 - SSE Parser 具体运行在哪条线程属于浏览器实现细节,标准并未要求必须在网络线程或主线程。
- JavaScript 事件回调最终在 EventSource 所属的执行环境中运行。普通页面通常是 Renderer Main Thread;Worker 中的 EventSource 则对应 Worker 的事件循环。
event.data是字符串,浏览器不会自动执行JSON.parse()。- SSE 事件边界由空行确定,而不是由 TCP 包或某次网络读取确定。
10.4 WebSocket
1 | const ws = new WebSocket("wss://example.com/socket"); |
对于经典 HTTP/1.1 WebSocket 握手:
1 | 客户端发送 HTTP Upgrade 请求 |
在较新的协议环境中,也存在基于 HTTP/2 扩展 CONNECT 等建立 WebSocket 的方式,因此“WebSocket 永远通过 HTTP/1.1 Upgrade”不是绝对规则。
无论底层如何建立,WebSocket 都具有自己的消息分帧规则。它与 TCP 字节流不同:
- TCP 本身没有消息边界。
- WebSocket Frame 提供协议级分帧。
- 一个 WebSocket Message 仍可能由多个 Frame 构成。
- 浏览器向 JavaScript 分发的是整理后的消息事件。
11. 服务器端:监听端口与处理 HTTP 请求
服务器程序通常是启动一次并长期运行的进程,不会为每次请求重新启动整个应用。
11.1 服务器启动阶段
以 Node.js 和 Express 为例:
1 | const express = require("express"); |
启动阶段可以近似理解为:
1 | 启动 Node.js 进程 |
listen(3000) 最终会使操作系统把发往对应地址和端口的新连接交给这个服务器。
11.2 监听 Socket 与连接 Socket
监听 Socket 用于接收新的连接。
当客户端建立 TCP 连接后,操作系统会为该连接维护独立状态,服务器可以得到一个连接 Socket:
1 | 监听 Socket:0.0.0.0:3000 |
这些连接共享服务器端口,但四元组不同,所以不会互相混淆。
可以把三层匹配记成:
1 | 目标地址和端口 |
在实际部署中,请求还可能先经过:
- 负载均衡器
- CDN
- 反向代理
- API Gateway
- Service Mesh
因此,公网端口不一定直接对应最终 Node.js 进程。
11.3 服务器接收请求的链路
1 | 客户端发送数据 |
网络数据到达并不会直接调用 Express 路由。中间必须经过:
- 操作系统协议栈
- Socket 缓冲区
- Node.js / libuv 网络层
- HTTP Parser
- Node.js HTTP 模块
- Express 中间件和路由匹配
11.4 TCP 字节流与 HTTP 消息边界
TCP 提供连续、有序字节流,但不提供业务消息边界。
客户端连续写入:
1 | Hello |
服务端可能读取到:
1 | HelloWorld |
也可能:
1 | Hel |
或者其他组合。
因此:
一次 Socket 可读事件、一次
recv()、一次 TCP Segment,都不等于一个完整 HTTP 请求。
HTTP 实现必须维护解析状态,并依据协议规则识别边界。
HTTP/1.1 中可能依据:
- 请求行和响应行
- Header 结束空行
Content-LengthTransfer-Encoding: chunked- 连接关闭语义
HTTP/2 和 HTTP/3 则有自己的 Frame 和 Stream 机制。
同一个 TCP 连接也不一定只处理一个 HTTP 请求:
- HTTP/1.1 Keep-Alive 可以顺序复用连接。
- HTTP/1.1 Pipelining 在实践中较少使用。
- HTTP/2 可以在一条连接中并发多个 Stream。
- HTTP/3 可以在 QUIC 连接中并发多个 Stream。
12. Node.js 服务器的运行与请求调度
Node.js 单个进程通常使用一条主 JavaScript 线程执行用户 JavaScript。
底层网络 I/O 由操作系统和 libuv 协作处理。部分文件系统、DNS、加密等工作可能进入 libuv 线程池,但不能简单地说所有异步操作都由线程池完成。
同一时刻,在一条 Node.js 主线程上通常只能执行一段 JavaScript。
12.1 每个请求调用一次处理函数
1 | app.get("/users/:id", handler); |
三个请求到达后,可以近似理解为多次调用同一函数:
1 | handler(reqA, resA); |
函数代码只有一份,但每次调用拥有独立的:
- 参数
- 局部变量
- 闭包状态
- Promise 链
- 请求和响应对象
- 执行进度
同时:
1 | 函数局部状态 |
重要业务状态通常不应只保存在单进程全局变量中,因为:
- 进程重启会丢失。
- 多进程部署时各进程拥有不同内存副本。
- 水平扩容后不同机器之间也不共享内存。
12.2 只有同步代码时
如果 A、B、C、D 四个请求对应的 JavaScript 回调已经就绪,而处理函数完全同步,则主线程会逐个执行:
1 | 执行 A 的同步 JavaScript |
A 的同步 JavaScript 没有结束前,其他请求的 JavaScript 回调不能插入当前调用栈。
12.3 遇到异步 I/O 时
1 | app.get("/users/:id", async (req, res) => { |
简化流程:
1 | 执行请求 A 的同步部分 |
暂停的是:
A 这一次 async 函数调用的后续执行。
不是:
整个 Node.js 进程或整个主线程一直阻塞等待数据库。
异步结果不会抢占当前正在执行的同步 JavaScript。它必须等待当前调用栈退出,并按照 Node.js 的事件循环和微任务规则获得执行机会。
请求可能:
1 | 按 A、B、C 的顺序开始 |
因为各自等待的数据库、文件或网络 I/O 耗时不同。
13. 慢函数与事件循环阻塞
判断服务器是否被阻塞,关键不只是函数总耗时,而是:
这段时间里,主 JavaScript 线程是否持续被同步代码占用。
13.1 慢异步 I/O
1 | const user = await database.findUser(id); |
即使数据库查询需要十秒:
- 当前请求会很慢。
- 当前 async 函数会在
await处暂停。 - 主 JavaScript 线程可以处理其他就绪任务。
但异步并不意味着资源无限。仍可能受限于:
- 数据库连接池
- 最大并发连接数
- Socket 数量
- 内存
- 请求队列
- 下游服务容量
- 超时策略
- 文件描述符上限
13.2 慢同步计算
1 | app.get("/calculate", (req, res) => { |
这十秒中:
- 当前 Node.js 主线程无法执行其他 JavaScript 回调。
- 操作系统仍可能接收网络数据。
- 内核和运行时缓冲区可能暂存部分数据。
- 请求可能排队。
- 缓冲区和队列可能持续增长。
- 客户端可能超时或断开。
async 关键字不会自动把同步计算移出主线程:
1 | async function calculate() { |
适合处理 CPU 密集任务的方案包括:
- Worker Threads
- 子进程
- 多个工作进程
- 独立计算服务
- 后台任务队列
- 原生模块或专门计算平台
典型多进程部署:
1 | 负载均衡器 / 反向代理 |
多进程可以利用多个 CPU 核心并隔离部分故障,但某一工作进程中的长时间同步 JavaScript,仍会阻塞该进程。
14. 一次 HTTP 请求的浏览器与服务器完整链路
下面将浏览器端和服务器端串联起来。
1 | 页面中的 JavaScript 调用 fetch() |
需要特别区分:
1 | fetch Promise 兑现 |
不等于:
1 | 整个响应体已经下载完成 |
前者通常在响应头可用时发生,后者需要进一步消费响应体。
15. 浏览器内部完整模型
可以把浏览器内部各部分的协作汇总为:
1 | Browser Process |
15.1 一次 fetch 的核心链路
1 | JavaScript 调用 fetch() |
15.2 一次 SSE 消息的核心链路
1 | 服务器在长期 HTTP Response 中写入 SSE 文本 |
16. 常见误解修正
误解一:V8 是浏览器引擎
不准确。
更准确地说:
1 | Blink:渲染引擎 / Web 平台实现 |
两者共同位于 Chromium Renderer 中并相互协作。
误解二:fetch 是 V8 实现的
不准确。
fetch 是浏览器提供的 Web API。V8 执行 JavaScript 对 fetch() 的调用,再通过绑定层进入 Blink 和网络系统。
误解三:V8 负责页面 Event Loop
不准确。
HTML 标准定义页面事件循环语义,浏览器运行时负责调度和实现;V8负责执行调度过程中需要运行的 JavaScript。
误解四:浏览器一定有一条固定的 Network Thread
不准确。
“网络线程”是教学简化。现代 Chromium 使用 Network Service,内部可包含不同线程、任务循环和平台 I/O 机制,并可能运行在独立进程中。
误解五:Network Service 收到数据后全部交给 Blink 解析
过于笼统。
不同层级由不同组件处理:
1 | TCP / TLS / HTTP |
误解六:fetch Promise 在整个响应体下载完成后才兑现
错误。
fetch Promise 通常在响应头可用并创建 Response 后兑现。响应体可以继续流式到达。
误解七:一次 Socket 可读就等于一个 HTTP 请求或 SSE Event
错误。
TCP 是字节流,没有应用消息边界。HTTP Parser、SSE Parser 或业务协议解析器必须维护缓冲区并识别边界。
误解八:每执行一个 Task,浏览器必定渲染一次
错误。
任务结束后通常会进行微任务检查点,但是否立即渲染由刷新时机、页面状态和浏览器调度决定。
误解九:SSE Parser 像 V8 一样执行代码
错误。
SSE Parser 是一个协议解析组件,只解析文本格式并生成事件。V8 是 JavaScript 引擎,负责执行事件回调中的 JavaScript。
误解十:异步就是多线程
错误。
异步表示当前 JavaScript 不需要同步等待结果。底层可能使用:
- 操作系统异步 I/O
- 事件通知
- 其他线程
- 其他进程
- 硬件
- 任务队列
异步是一种控制流和调度方式,不等同于特定线程模型。
17. 复习清单
- 浏览器是多进程、多线程、多个运行时组件协作的系统。
- Browser Process、Network Service、Renderer Process 和 GPU Process 承担不同职责。
- Network Service 在现代 Chromium 中可能是独立进程,不应简单等同于 Browser Process 内的一条固定 Network Thread。
- Blink 是 Chromium 的渲染引擎和 Web 平台实现,负责 DOM、HTML/CSS、Web API、布局和事件分发等能力。
- V8 是 JavaScript 引擎,负责解析、编译和执行 JavaScript。
- V8 可以近似理解为 JavaScript 执行器,但还负责 JIT、对象内存、垃圾回收等 JavaScript 引擎能力。
- V8 不负责 TCP、HTTP、DOM、CSS、Layout、Paint 或 SSE 协议解析。
- JavaScript 调用
fetch、document、setTimeout、EventSource时,会通过绑定层进入 Blink 或其他浏览器实现。 - 浏览器需要执行 JavaScript 回调时,会调用 V8。
- 页面 Event Loop 不是 V8 单独拥有的;它是浏览器页面运行环境的调度机制。
- Task 执行完成并退出 JavaScript 调用栈后,通常会进行 Microtask checkpoint。
- 微任务检查点会持续执行微任务,直到当前微任务队列为空。
- 浏览器不保证每个 Task 后都立即渲染。
- TCP/IP 协议栈主要由操作系统实现。
- Socket 是操作系统维护的通信端点,Socket API 是程序使用网络能力的接口。
- TCP 提供可靠、有序的字节流,但不提供 HTTP、SSE 或业务消息边界。
- HTTP 实现负责把结构化消息编码为字节,并从字节中解析 HTTP 语义。
epoll_wait()、IOCP、kqueue等是平台 I/O 等待机制,不是 Web 标准规定的浏览器结构。wakeupFd可以帮助理解跨线程唤醒,但不能视为每次 Fetch 都必须经过的标准步骤。- 一次
fetch()不一定创建新 Socket,可能复用连接、命中缓存或经过 Service Worker。 fetchPromise 通常在响应头可用时兑现,而不是等待整个响应体完成。response.json()、response.text()等需要继续读取并消费响应体。Response.body是ReadableStream,可以逐步读取响应体。- 一次 Stream 读取不一定对应一个完整业务消息。
- SSE 是一个长期不结束的 HTTP 响应。
- SSE Parser 根据空行识别事件边界,而不是根据 TCP 包或网络 Chunk。
- SSE Parser 不是 V8,其具体运行线程属于浏览器实现细节。
- 一个完整 SSE Event 解析完成后,浏览器会安排事件分发任务。
- EventSource 回调最终由对应执行环境中的 V8 执行。
- WebSocket 提供双向通信和协议级消息分帧。
- 服务器监听端口,本质上是创建监听 Socket 并让操作系统将新连接交给服务器。
- 监听 Socket 负责接收新连接,每条 TCP 连接有独立连接状态和缓冲区。
- 目标地址和端口帮助找到服务,TCP 四元组区分连接,HTTP 方法和路径匹配应用处理逻辑。
- Node.js 单进程通常由一条主 JavaScript 线程执行用户代码。
- Node.js 底层网络 I/O 由操作系统和 libuv 协作完成。
- Node.js 的同步代码不会被其他 JavaScript 请求处理逻辑抢占。
await暂停的是当前 async 函数的后续执行,不是整个进程。- 慢异步 I/O 不会持续占用主 JavaScript 线程,但仍消耗连接、内存和下游容量。
- 长时间同步计算会阻塞当前 Node.js 工作进程。
- CPU 密集任务通常需要 Worker Threads、多进程或独立计算服务。
最终心智模型
把浏览器理解成一个完整运行时:
1 | 浏览器运行时 |
而 V8:
1 | V8 |
因此,最值得记住的一句话是:
浏览器负责组织整个网页运行环境,V8 负责在这个环境中执行 JavaScript。
或者用更形象但不完全严格的比喻:
1 | 浏览器运行时 ≈ 操作系统和调度环境 |
这个比喻不是完整的工程事实,但非常适合建立最初的职责边界。

