应该有很多前端开发人员都思考过这么一个问题:从输入 URL 到页面加载完成,中间都做发生了什么?
这个问题涉及的面非常广,每个涉及的点又很深入。从触屏/键盘如何到 CPU?CPU 如何到系统内核?如何从操作系统 GUI 到浏览器?浏览器如何向网卡发送数据?数据如何从本机网卡发送到服务器?服务器接收数据后如何处理?服务器返回数据后浏览器如何处理?浏览器如何将页面展现出来?等等等等,每一个过程都包含了大量且深入的知识体系,很难一以贯通。
但作为前端开发人员,浏览器是我们的主要工具之一,浏览器是如何将页面展现出来的则是我们更关注的部分。因此本文就从一些基本流程来简要描述这个过程。
从上面这个图中可以发现,虽然使用的 Javascript 是单线程语言,但浏览器本身是多进程的。
但是这并不是从一而终的状态,而是浏览器从早期的单进程结构逐渐发展发展而来。现代浏览器各进程根据负责的功能不同,分为浏览器进程、渲染器进程、网络进程、GPU 进程、缓存进程、插件进程等等。为了更好的理解浏览器页面的呈现过程,我们以最主流的 Chrome 为例,简要的说明一下各个进程的大致职能:
浏览器进程: 负责控制界面展示、用户交互、子进程管理等功能。
渲染器进程: 负责将 HTML\CSS\JS 转化为用户可以与之交互的网页。渲染引擎如 webkit、blink 和 JS 引擎 V8 都是在该进程之中。
GPU 进程: GPU 进程原本是为了实现 3D CSS 效果,但是随后页面、Chrome 的 UI 都采用 GPU 来绘制,是 GPU 成为了重要需求,于是增加了 GPU 进程。
网络进程: 负责页面的网络资源加载。
插件进程:负责插件的运行,由于插件可能崩溃,需要插件进程其他进程隔离。注意,插件并不是我们常用的浏览器拓展,plugin 和 extension 是不同的。
缓存进程:负责处理页面资源缓存和清理。
我们本次需要重点关注的是渲染器进程。
回到问题,当我们在浏览器地址栏输入地址时,浏览器进程的 UI 线程会捕捉输入内容,如果访问的是网址,那么 UI 线程会启动一个网络线程来构建请求(这里我们暂时不考虑缓存,缓存又是另外一个故事了),它请求 DNS 进行域名解析然后连接服务器获取数据。如果我们输入的是关键词,浏览器则使用默认配置的搜索引擎来搜索。在获取到数据并通过安全校验后,网络线程会通知 UI 线程数据准备完毕,然后UI线程创建一个渲染器进程来进行页面的渲染,并将数据通过 IPC 管道传递给渲染器进程。
至此,我们的主角渲染器进程登场!
解析 HTML渲染器进程接收到的是一个 HTML,需要把 HTML 解析成 DOM 数据结构。因为直接的 HTML 字节流是无法被渲染引擎所理解的,必须转化成可以理解的内部结构。这个内部结构就是 DOM,DOM 提供了对 HTML 文档的结构化表述。在渲染引擎中,DOM 有三个层面的作用:
从页面角度:DOM 是生成页面的基础数据结构。
从 js 角度:DOM 提供了 js 操作的接口。通过这套接口,js 可以对 DOM 接口进行访问,从而使开发者拥有改变文档结构、样式、内容的能力。
从安全角度:DOM 是 HTML 经过解析的内部数据结构,它将 web 页面和 js 链接起来,并过滤了一些不安全的内容。
渲染器进程内部使用 HTML Parser 将 HTML 解析成 DOM 结构。需要注意的是,HTML 解析器不会等待整个 HTML 文档加载完毕再去解析,而是加载多少了多少 HTML,就解析多少。
那么 HTML 字节流是如何转换成 DOM 的呢?
其实和 V8 解析 js 类似,也是做词法分析,通过分词器将字节流成功成一个个 token,包括 Tag token 和文本 token。HTML 解析器维护了一个 token 栈结构,token 会按照对应顺序入栈出栈,然后将 token 解析成 DOM 节点,并将 DOM 节点添加进 DOM 树中。
前面提到生成 DOM 可以过滤一些不安全内容。这主要是渲染引擎中的一个名为XSSAuditor 安全检查模块实现的。它会监测词法安全,在分词器解析出 token 之后,检查这些模块是否引用了外部脚本,是否符合 CSP 规范,是否存在跨站点请求等。如果出现不符合规范的内容。XSSAuditor 会对该脚本或下载任务进行拦截。
DOM 树在构建过程中会创建 document 对象,然后以 document 为根节点的 DOM 树不断修改向其中添加新的元素。
解析 CSS