IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    「转」从输入 URL 到页面加载的过程 · 看不见我的美 · 是你瞎了眼

    馬腊咯稽发表于 2019-07-26 00:00:00
    love 0
    「转」从输入 URL 到页面加载的过程

    梳理主干流程

    1. 从浏览器接收 URL 到开启网络请求线程(浏览器的机制、进程与线程之间的关系);
    2. 开启网络线程到发出一个完整的 HTTP 请求(DNS 查询、TCP/IP 请求、五层因特网协议栈);
    3. 从服务器接收到请求到对应后台接收到请求(负载均衡、安全拦截、后台内部的处理);
    4. 后台和前台的 HTTP 交互(HTTP 头部、响应码、报文结构、Cookie 优化、编码解码);
    5. 缓存问题,HTTP 的缓存(HTTP 缓存头部、ETag、Catch-Control);
    6. 浏览器接收到 HTTP 数据包后的解析流程(解析 HTML、词法分析、DOM 树、CSS 规则树、渲染树、Layout、Paint、渲染、复合图层的合成、GPU 绘制、外链资源的处理、loaded、DOMContentLoaded);
    7. CSS 的可视化格式模型(元素的渲染规则);
    8. JS 引擎解析过程(解释阶段,预处理阶段、执行阶段生成执行上下文、作用域链、回收机制);
    9. 拓展(跨域、Web 安全、Hybrid 模式)。

    从浏览器接收 URL 到开启网络请求线程

    这一部分展开的内容是:浏览器进程/线程模型,JS 运行机制。

    一、多进程的浏览器

    浏览器是多进程的,有一个主控进程,以及每一个 Tab 页都会新开一个进程(某些情况下多个 Tab 会合并进程);进程可能包括主控进程、插件进程、GPU、Tab 页(浏览器内核)等:

    • Browser 进程:浏览器的主进程(负责协调、主控),只有一个;
    • 第三方插件进程:每种类型的插件对应一个进程,仅当使用该插件时才创建;
    • GPU 进程:最多一个,用于 3D 绘制;
    • 浏览器渲染进程(内核):默认每个 Tab 页一个进程,互不影响,控制页面渲染,脚本执行,事件处理等(有时候会优化,如多个空白 Tab 会合并成一个进程)。

    二、多线程的浏览器内核

    每一个 Tab 页可以看作是浏览器内核进程,然后这个进程是多线程的,它有几大类子线程:

    • GUI 渲染线程;
    • JS 引擎线程;
    • 事件触发线程;
    • 定时器线程;
    • 网络请求线程。

    三、解析 URL

    输入 URL 后,会进行解析(URL 的本质就是统一资源定位符),URL 一般包括几大部分:

    • protocol,协议头,譬如有 http、ftp 等;
    • host,主机域名或 IP 地址;
    • port,端口号;
    • path,目录路径;
    • query,查询参数;
    • fragment,即 # 后的 hash 值。

    四、网络请求都是单独的线程

    每次网络请求时都需要开辟单独的线程进行,如果 URL 解析到 HTTP 协议,就会新建一个网络线程去处理资源下载。

    更多参考: 从浏览器多进程到 JS 单线程,JS 运行机制最全面的一次梳理 。

    开启网络线程到发出一个完整的 HTTP 请求

    这一部分主要内容包括:DNS 查询,TCP/IP 请求构建,五层因特网协议栈等。

    一、DNS 查询得到 IP

    如果输入的是域名,需要进行 DNS 解析成 IP,大致流程:

    • 如果浏览器有缓存,直接使用浏览器缓存,否则使用本机缓存,再没有的话就是用 host;
    • 如果本地没有,就向 DNS 域名服务器查询(中间可能还会经过路由,也有缓存等),查询到对应的 IP。

    注意:域名查询时有可能是经过了 CDN 调度器的(如果有 CDN 存储功能的话);DNS 解析是很耗时的,如果解析域名过多,会让首屏加载变得过慢,可以考虑 dns-prefetch 优化。

    二、TCP/IP 请求

    HTTP 的本质就是 TCP/IP 请求,TCP 将 HTTP 长报文划分为短报文,通过三次握手与服务端建立连接,进行可靠传输。

    1. 三次握手的步骤

    1. 客户端:hello,你是 server 么?
    2. 服务端:hello,我是 server,你是 client 么?
    3. 客户端:yes,我是 client。

    建立连接成功后,接下来就正式传输数据;待到断开连接时,需要进行四次挥手。

    2. 四次挥手的步骤

    1. 主动方:我已经关闭了向你那边的主动通道了,只能被动接收了;
    2. 被动方:收到通道关闭的信息;
    3. 被动方:那我也告诉你,我这边向你的主动通道也关闭了;
    4. 主动方:最后收到数据,之后双方无法通信。

    3. TCP/IP 的并发限制

    浏览器对同一域名下并发的 TCP 连接是有限制的(2-10 个不等);在 HTTP 1.0 中往往一个资源下载就需要对应一个 TCP/IP 请求,所以针对这个瓶颈,又出现了很多的资源优化方案。

    4. GET 和 POST 的区别

    GET 和 POST 虽然本质都是 TCP/IP,但两者除了在 HTTP 层面外,在 TCP/IP 层面也有区别;GET 会产生一个 TCP 数据包,POST 两个,具体就是:

    • GET 请求时,浏览器会把 Headers 和 Data 一起发送出去,服务器响应 200(返回数据);
    • POST 请求时,浏览器先发送 Headers,服务器响应 100,浏览器再发送 data,服务器响应 200(返回数据)。

    5. 五层因特网协议栈

    从应用层的发送 HTTP 请求,到传输层通过三次握手建立 TCP/IP 连接,再到网络层的 IP 寻址,再到数据链路层的封装成帧,最后到物理层的利用物理介质传输。

    五层因特网协议栈其实就是:

    1. 应用层(DNS、HTTP)DNS 解析成 IP 并发送 HTTP 请求;
    2. 传输层(TCP、UDP)建立 TCP 连接(三次握手);
    3. 网络层(IP、ARP)IP 寻址;
    4. 数据链路层(PPP)封装成帧;
    5. 物理层(利用物理介质传输比特流)物理传输(然后传输的时候通过双绞线,电磁波等各种介质)。

    当然,其实也有一个完整的 OSI 七层框架,与之相比,多了会话层、表示层。

    OSI 七层框架:物理层、数据链路层、网络层、传输层、会话层、表示层、应用层:

    • 表示层:主要处理两个通信系统中交换信息的表示方式,包括数据格式交换、数据加密与解密、数据压缩与终端类型转换等;
    • 会话层:它具体管理不同用户和进程之间的对话,如控制登陆和注销过程。

    从服务器接收到请求到对应后台接收到请求

    服务端在接收到请求时,内部会进行很多的处理。

    一、负载均衡

    对于大型的项目,由于并发访问量很大,所以往往一台服务器是吃不消的,所以一般会有若干台服务器组成一个集群,然后配合反向代理实现负载均衡,当然了,负载均衡不止这一种实现方式,这里不深入。

    用户发起的请求都指向调度服务器(反向代理服务器,譬如安装了 nginx 控制负载均衡),然后调度服务器根据实际的调度算法,分配不同的请求给对应集群中的服务器执行,然后调度器等待实际服务器的 HTTP 响应,并将它反馈给用户。

    二、后台处理

    一般后台都是部署到容器中的,所以一般为:

    • 先是容器接受到请求(如 tomcat);
    • 对应容器中的后台程序接收到请求(如 Java);
    • 后台会有自己的统一处理,处理完后响应结果。

    概括下:

    • 一般,后端是有统一的验证的,如安全拦截、跨域验证;
    • 如果这一步不符合规则,就直接返回了相应的 HTTP 报文(如拒绝请求等);
    • 当验证通过后,才会进入实际的后台代码,此时是程序接收到请求,然后执行(如查询数据库、大量计算等等);
    • 程序执行完毕后,就会返回一个 HTTP 响应包(一般这一步也会经过多层封装);
    • 将这个包从后端发送到前端,完成交互。

    后台和前台的 HTTP 交互

    前后端交互时,HTTP 报文作为信息的载体,所以 HTTP 是一块很重要的内容。

    一、HTTP 报文结构

    报文一般包括了:通用头部、请求/响应头部、请求/响应体。

    1. 通用头部

    1. Request URL:请求的 Web 服务器地址;
    2. Request Method:GET、POST、OPTIONS、PUT、HEAD、DELETE、CONNECT、TRACE;
    3. Status Code:请求的返回状态码;
    4. Remote Address:请求的远程服务器地址(会转为 IP)。

    譬如,在跨域拒绝时,可能是 Method 为 OPTIONS,状态码为 404/405 等。

    • HTTP/1.0 定义了三种请求方法:GET、POST 和 HEAD 方法;
    • HTTP/1.1 定义了八种请求方法:GET、POST、HEAD、OPTIONS、PUT、DELETE、TRACE 和 CONNECT 方法。

    不同范围状态的意义:

    • 1xx——指示信息,表示请求已接收,继续处理;
    • 2xx——成功,表示请求已被成功接收、理解、接受;
    • 3xx——重定向,要完成请求必须进行更进一步的操作;
    • 4xx——客户端错误,请求有语法错误或请求无法实现;
    • 5xx——服务器端错误,服务器未能实现合法的请求。

    2. 请求/响应头部

    常用的请求头部:

    • Accept:接收类型,表示浏览器支持的 MIME 类型(对标服务端返回的 Content-Type);
    • Accept-Encoding:浏览器支持的压缩类型,如 gzip 等;
    • Cache-Control:指定请求和响应遵循的缓存机制,如 no-cache 等;
    • If-Modified-Since:对应服务端的 Last-Modified,用来匹配看文件是否变动;
    • Expires:缓存控制,在这个时间内不会请求,直接使用缓存;
    • Max-Age:代表资源在本地缓存多少秒,有效时间内不会请求,而是使用缓存;
    • If-None-Match:对应服务端的 ETag,用来匹配文件内容是否改变(非常精确);
    • Cookie:同域访问时会自动带上;
    • Connection:当浏览器与服务器通信时对长连接如何进行处理,如 keep-alive;
    • Host:请求的服务器 URL;
    • Origin:最初的请求是从哪里发起的(只会精确到端口),Origin 比 Referer 更尊重隐私;
    • Referer:该页面的来源 URL(适用于所有类型的请求,会精确到详细页面地址,csrf 拦截常用到这个字段);
    • User-Agent:用户客户端的一些必要信息。

    常用的响应头部:

    • Access-Control-Allow-Headers:服务器端允许的请求 Headers;
    • Access-Control-Allow-Methods:服务器端允许的请求方法;
    • Access-Control-Allow-Origin:服务器端允许的请求 Origin 头部;
    • Content-Type:服务端返回的实体内容的类型;
    • Date:数据从服务器发送的时间;
    • Last-Modified:请求资源的最后修改时间;
    • Expires:应该在什么时候认为文档已经过期,从而不再缓存它;
    • Max-Age:客户端的本地资源应该缓存多少秒,开启了 Cache-Control 后有效;
    • ETag:请求变量的实体标签的当前值;
    • Set-Cookie:设置和页面关联的 Cookie,服务器通过这个头部把 Cookie 传给客户端;
    • Keep-Alive:如果客户端有 keep-alive,服务端也会有响应;
    • Server:服务器的一些相关信息。

    一般来说,请求头部和响应头部是匹配分析的;譬如,请求头部的 Accept 要和响应头部的 Content-Type 匹配,否则会报错。

    3. 请求/响应实体

    HTTP 请求时,除了头部,还有消息实体,一般来说,请求实体中会将一些需要的参数都放入;譬如实体中可以放参数的序列化形式(a=1&b=2),或者直接放表单对象(FormData 对象,上传时可以夹杂参数以及文件);而一般响应实体中,就是放服务端需要传给客户端的内容。

    二、Cookie 以及优化

    Cookie 是浏览器的一种本地存储方式,一般用来帮助客户端和服务端通信的,常用来进行身份校验,结合服务端的 session 使用:

    1. 在登录页面,用户登录了;
    2. 此时,服务端会生成一个 session,session 中有对应用户的信息(如用户名、密码等);
    3. 然后会有一个 sessionid(相当于是服务端的这个 session 对应的 key);
    4. 然后服务端在登录页面中写入 Cookie,值就是 :jsessionid=xxx;
    5. 然后浏览器本地就有这个 Cookie 了,以后访问同域名下的页面时,自动带上 Cookie,自动检验,在有效时间内无需二次登陆。

    上述就是 Cookie 的常用场景简述(当然了,实际情况下得考虑更多因素)。

    一般来说,Cookie 是不允许存放敏感信息的(千万不要明文存储用户名、密码),因为非常不安全;如果一定要强行存储,首先,一定要在 Cookie 中设置 HTTPOnly(这样就无法通过 JS 操作了),另外可以考虑 RSA 等非对称加密(因为浏览器本地也是容易被攻克的,并不安全)。

    另外,由于在同域名的资源请求时,浏览器会默认带上本地的 Cookie,针对这种情况,在某些场景下是需要优化的:

    1. 客户端在域名 A 下有 Cookie;
    2. 然后在域名 A 下有一个页面,页面中有很多依赖的静态资源(都是域名 A 的,譬如有 20 个静态资源);
    3. 此时就有一个问题,页面加载,请求这些静态资源时,浏览器会默认带上 Cookie;
    4. 也就是说,这 20 个静态资源的 HTTP 请求,每一个都得带上 Cookie,而实际上静态资源并不需要 Cookie 验证。

    此时就造成了较为严重的浪费,而且也降低了访问速度;当然了,针对这种场景,是有优化方案的(多域名拆分)。具体做法就是:

    • 将静态资源分组,分别放到不同的域名下(如 static.base.com);
    • 而 page.base.com(页面所在域名)下请求时,是不会带上 static.base.com 域名的 Cookie 的,避免了浪费。

    说到了多域名拆分,这里再提一个问题,那就是:

    • 在移动端,如果请求的域名数过多,会降低请求速度(因为域名整套解析流程是很耗费时间的);
    • 此时就需要用到一种优化方案:dns-prefetch(让浏览器空闲时提前解析 DNS 域名,勿滥用)。

    三、gzip 压缩

    首先,明确 gzip 是一种压缩格式,需要浏览器支持才有效,而且 gzip 压缩效率很好(高达 70% 左右);gzip 一般是由 apache、tomcat 等服务器开启。

    除了 gzip 外,也还会有其它压缩格式(如 deflate,没有 gzip 高效,且不流行),所以一般只需要在服务器上开启了 gzip 压缩,之后的请求就都是基于 gzip 压缩格式的,非常方便。

    gzip 有(0~9)10 个压缩机别,压缩级别越高,压缩效果越好,也越占 CPU;考虑都性能和压缩率的取舍,压缩级别不易设的太高。

    四、长连接与短连接

    TCP/IP 层面的定义:

    • 长连接:一个 TCP/IP 连接上可以连续发送多个数据包;在 TCP 连接保持期间,如果没有数据包发送,需要双方发检测包以维持此连接,一般需要自己做在线维持(类似于心跳包);
    • 短连接:通信双方有数据交互时,就建立一个 TCP 连接,数据发送完成后,则断开此 TCP 连接。

    HTTP 层面的定义:

    • HTTP/1.0 中,默认使用的是短连接;浏览器每进行一次 HTTP 操作,就建立一次连接,任务结束就中断连接;譬如每一个静态资源请求时都是一个单独的连接;
    • HTTP/1.1 起,默认使用长连接,使用长连接会有这一行 Connection: keep-alive;在长连接的情况下,当一个网页打开完成后,客户端和服务端之间用于传输 HTTP 的 TCP 连接不会关闭;如果客户端再次访问这个服务器的页面,会继续使用这一条已经建立的连接。

    注意:keep-alive 不会永远保持,它有一个持续时间,一般在服务器中配置(如 apache);另外长连接需要客户端和服务器都支持时才有效。

    五、HTTP/2

    HTTP/2 与 HTTP/1.x 的显著不同点:

    • HTTP/1.x 中,每请求一个资源,都是需要开启一个 TCP/IP 连接的;所以对应的结果是,每一个资源对应一个 TCP/IP 请求,由于 TCP/IP 本身有并发数限制,所以当资源一多,速度就显著慢下来;
    • HTTP/2 中,一个 TCP/IP 请求可以请求多个资源;也就是说,只要一次 TCP/IP 请求,就可以请求若干个资源,分割成更小的帧请求,速度明显提升。

    所以,如果 HTTP/2 全面应用,很多 HTTP/1.x 中的优化方案就无需用到了(如打包成精灵图、静态资源多域名拆分等)。

    简述下 HTTP/2 的一些特性:

    • 多路复用(即一个 TCP/IP 连接可以请求多个资源);
    • 首部压缩(HTTP 头部压缩,减少体积);
    • 二进制分帧(在应用层跟传送层之间增加了一个二进制分帧层,改进传输性能,实现低延迟和高吞吐量);
    • 服务器端推送(服务端可以对客户端的一个请求发出多个响应,可以主动通知客户端);
    • 请求优先级(如果流被赋予了优先级,它就会基于这个优先级来处理,由服务器决定需要多少资源来处理该请求)。

    六、HTTPS

    HTTPS 就是安全版本的 HTTP,譬如一些支付等操作基本都是基于 HTTPS 的,因为 HTTP 请求的安全系数太低了;简单来看,HTTPS 与 HTTP 的区别就是:在请求前,会建立 SSL 链接,确保接下来的通信都是加密的,无法被轻易截取分析。

    SSL/TLS 的握手流程:

    1. 浏览器请求建立 SSL 链接,并向服务端发送一个随机数 ClientRandom 和客户端支持的加密方法,比如 RSA 加密,此时是明文传输;

    2. 服务端从中选出一组加密算法与 Hash 算法,回复一个随机数 ServerRandom,并将自己的身份信息以证书的形式发回给浏览器(证书里包含了网站地址,非对称加密的公钥,以及证书颁发机构等信息);

    3. 浏览器收到服务端的证书后:

      • 验证证书的合法性(颁发机构是否合法,证书中包含的网址是否和正在访问的一样),如果证书信任,则浏览器会显示一个小锁头,否则会有提示;
      • 接收证书后(不管信不信任),浏览器会生产新的随机数 PremasterSecret,然后证书中的公钥以及指定的加密方法加密 PremasterSecret,发送给服务器;
      • 利用 ClientRandom、ServerRandom 和 PremasterSecret 通过一定的算法生成 HTTP 链接数据传输的对称加密 key;
      • 使用约定好的 Hash 算法计算握手消息,并使用生成的 key 对消息进行加密,最后将之前生成的所有信息发送给服务端。
    4. 服务端收到浏览器的回复:

      • 利用已知的加解密方式与自己的私钥进行解密,获取 PremasterSecret;
      • 和浏览器相同规则生成 key;
      • 使用 key 解密浏览器发来的握手消息,并验证 Hash 是否与浏览器发来的一致;
      • 使用 key 加密一段握手消息,发送给浏览器。
    5. 浏览器解密并计算握手消息的 Hash,如果与服务端发来的 Hash 一致,此时握手过程结束。

    之后所有的 HTTPS 通信数据将由之前浏览器生成的 key 并利用对称加密算法进行加密。

    HTTP 缓存

    前后端的 HTTP 交互中,使用缓存能很大程度上的提升效率:

    • 强缓存(200)时,浏览器如果判断本地缓存未过期,就直接使用,无需发起 HTTP 请求;
    • 协商缓存(304)时,浏览器会向服务端发起 HTTP 请求,然后服务端告诉浏览器文件未改变,让浏览器使用本地缓存。

    对于协商缓存,强制刷新可以使得缓存无效;对于强缓存,在未过期时,必须更新资源路径才能发起新的请求。各大缓存头部的整体关系如下图:

    解析页面流程

    浏览器内核拿到内容后,渲染步骤大致可以分为以下几步:

    1. 解析 HTML,构建 DOM 树;
    2. 解析 CSS,生成 CSS 规则树;
    3. 合并 DOM 树和 CSS 规则树,生成 render 树;
    4. 布局 render 树(layout/reflow),负责各元素尺寸、位置的计算;
    5. 绘制 render 树(paint),绘制页面像素信息;
    6. 浏览器会将各层的信息发送给 GPU,GPU 会将各层合成(composite),显示在屏幕上。

    一、解析 HTML,构建 DOM 树

    这个过程可以简述如下:bytes → characters → tokens → nodes → DOM;列举一些重点过程:

    1. Conversion 转换:浏览器将获得的 HTML 内容(bytes)基于他的编码转换为单个字符;
    2. Tokenizing 分词:浏览器按照 HTML 规范标准将这些字符转换为不同的标记 token;每个 token 都有自己独特的含义以及规则集;
    3. Lexing 词法分析:分词的结果是得到一堆的 token,此时把他们转换为对象,这些对象分别定义他们的属性和规则;
    4. DOM 构建:因为 HTML 标记定义的就是不同标签之间的关系,这个关系就像是一个树形结构一样。

    二、生成 CSS 规则

    CSS 规则树的生成也是类似;简述为:bytes → characters → tokens → nodes → CSSOM。

    三、构建渲染树

    当 DOM 树和 CSSOM 都有了后,就要开始构建渲染树了;一般来说,渲染树和 DOM 树相对应的,但不是严格意义上的一一对应;有一些不可见的 DOM 元素不会插入到渲染树中,如 head 这种不可见的标签或者 display:none 等。

    注意:DOM 解析和 CSS 解析是两个并行的进程,所以 CSS 加载不会阻塞 DOM 树的解析;RenderTree 是依赖于 DOMTree 和 CSSOMTree 的,所以无论 DOMTree 是否已经完成,它都必须等待到 CSSOMTree 构建完成,即 CSS 加载完成(或 CSS 加载失败)后,才能开始渲染。

    四、渲染

    有了渲染树,接下来就是开始渲染,基本流程如下:

    图中的线与箭头代表通过 JS 动态修改了 DOM 或 CSS,导致了 Reflow 或 Repaint:

    • Reflow(Layout),即回流;一般意味着元素的内容、结构、位置或尺寸发生了变化,需要重新计算样式和渲染树;
    • Repaint(Paint),即重绘;意味着元素发生的改变只是影响了元素的一些外观之类的时候(例如:背景色、边框颜色、文字颜色等),此时只需要应用新样式绘制这个元素就可以了。

    回流的成本开销要高于重绘,而且一个节点的回流往往回导致子节点以及同级节点的回流,所以优化方案中一般都包括“尽量避免回流”。

    什么会引起回流:

    1. 页面渲染初始化;
    2. DOM 结构改变,比如删除某节点;
    3. 渲染树变化,比如减少了 padding;
    4. 窗口 resize;
    5. 改变字体大小会引发回流;
    6. 获取某些属性会引发回流:
      • offset(Top/Left/Width/Height);
      • scroll(Top/Left/Width/Height);
      • cilent(Top/Left/Width/Height);
      • width、height;
      • IE’s currentStyle;
      • getBoundingClientRect();
      • getComputedStyle()。
    7. 回流一定伴随着重绘,重绘却可以单独出现。

    回流优化方案:

    1. 减少逐项更改样式,最好一次性更改,或者将样式定义为 class 并一次性更新;
    2. 避免循环操作 DOM,创建一个 documentFragment 或 div,在它上面应用所有 DOM 操作,最后再把它添加到 document;
    3. 避免多次读取 offset 等属性;无法避免则将它们缓存到变量;
    4. 将复杂的元素绝对定位或固定定位,使得它脱离文档流,否则回流代价会很高;
    5. 使用 transform 替代 position;
    6. 使用 visibility:hidden 替换 display:none,因为前者只会引起重绘,后者会引发回流;
    7. 避免使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局;
    8. 将频繁重绘或者回流的节点设置为图层,图层能够阻止该节点的渲染行为影响别的节点;例如 will-change、video、iframe 等标签,浏览器会自动将该节点变为图层;
    9. 使用硬件加速,可以让 transform、opacity、filters 这些动画不会引起回流重绘。

    示例:

    1
    2
    3
    4
    5
    6
    7
    
    var s = document.body.style;
    s.padding = '2px'; // 回流 + 重绘
    s.border = '1px solid red'; // 回流 + 重绘
    s.color = 'blue'; // 重绘
    s.backgroundColor = '#ccc'; // 重绘
    s.fontSize = '14px'; // 回流 + 重绘
    document.body.appendChild(document.createTextNode('abc')); // 添加 node,回流 + 重绘
    

    五、简单层与复合层

    上述中的渲染中止步于绘制,但实际上绘制这一步也没有这么简单,它可以结合复合层和简单层的概念来讲:

    • 可以认为默认只有一个复合图层,所有的 DOM 节点都是在这个复合图层下的;
    • 如果开启了硬件加速功能,可以将某个节点变成复合图层;
    • 复合图层之间的绘制互不干扰,由 GPU 直接控制;
    • 简单图层中,就算是 position:absolute 等布局,变化时不影响整体的回流;但是由于在同一个图层中,仍然是会影响绘制的,因此做动画时性能仍然很低;而复合层是独立的,所以一般做动画推荐使用硬件加速。

    更多参考: 普通图层和复合图层

    六、资源外链的下载

    实际上,在解析 HTML 时,会遇到一些资源连接,此时就需要进行单独处理了;简单起见,这里将遇到的静态资源分为一下几大类(未列举所有):

    • CSS 样式资源;
    • JS 脚本资源;
    • 图片类资源。

    1. 遇到外链时的处理

    当遇到上述的外链时,会单独开启一个下载线程去下载资源(HTTP/1.x 中,每一个资源的下载都要开启一个请求,对应一个 TCP/IP 链接)。

    2. 遇到 CSS 样式资源

    • CSS 的加载不会阻塞 DOM 树的构建;
    • CSS 的加载会阻塞 DOM 树的渲染;
    • mediaQuery 声明的 CSS 不会阻塞渲染。

    3. 遇到 JS 脚本资源

    • 阻塞浏览器的解析;发现一个外链脚本时,需等待脚本下载完成并执行后才会继续解析 HTML;
    • 现代浏览器在脚本阻塞时,会继续下载其它资源(当然有并发上限),虽然脚本可以并行下载,解析过程仍然是阻塞的;
    • 加上 async 或 defer,脚本就变成异步了,可以等到解析完毕后再执行;
    • async 是异步执行,异步下载完毕后就会执行,不确保执行顺序,一定在 load 前,但不确定在 DOMContentLoaded 的前或后;
    • defer 是延迟执行,效果像是将脚本放在了 body 后面一样。

    4. 遇到图片类资源

    异步下载,不会阻塞解析,下载完毕后直接用图片替换原有 src 的地方。

    七、load 和 DOMContentLoaded

    • load 事件触发时,页面上所有的 DOM、样式表、脚本、图片都已经加载完成了;
    • DOMContentLoaded 事件触发时,仅当 DOM 加载完成,不包括样式表、图片。

    CSS 可视化格式模型

    CSS 可视化格式模型就是规定了浏览器在页面中如何处理文档树:

    • 每一个元素都有自己的盒子模型;
    • 可视化格式模型则是把这些盒子按照规则摆放到页面上,也就是如何布局;
    • 盒子模型规定了怎么在页面里摆放盒子,盒子的相互作用等等。

    关键字:包含块(Containing Block)、控制框(Controlling Box)、BFC(Block Formatting Context)、IFC(Inline Formatting Context)、定位体系、浮动…

    一、包含块(Containing Block)

    一个元素的 Box 的定位和尺寸,会与某一矩形框有关,这个框就称之为包含块。元素会为它的子孙元素创建包含块,但是并不是说元素的包含块就是它的父元素,元素的包含块与它的祖先元素的样式等有关系:

    • 根元素是最顶端的元素,它没有父节点,它的包含块就是初始包含块;
    • position:static/relative 的包含块由它最近的块级、单元格或者行内块祖先元素的内容框(Content)创建;
    • position:fixed 的包含块是当前可视窗口;
    • position:absolute 的包含块由它最近的 position:absolute/relative/fixed 的祖先元素创建:
      • 如果祖先元素是行内元素,则包含块取决于其祖先元素的 direction 特性;
      • 如果祖先元素不是行内元素,那么包含块的区域应该是祖先元素的内边距边界。

    二、控制框(Controlling Box)

    块级元素和块框以及行内元素和行框的相关概念。

    1. 块框

    • 块级元素会生成一个块框(Block Box),块框会占据一整行,用来包含子 Box 和生成的内容;
    • 块框同时也是一个块包含框(Containing Box),里面要么只包含块框,要么只包含行内框(不能混杂),如果块框内部有块级元素也有行内元素,那么行内元素会被匿名块框包围;
    • 如果一个块框在其中包含另外一个块框,那么我们强迫它只能包含块框,因此其它文本内容生成出来的都是匿名块框。

    2. 行内框

    • 一个行内元素生成一个行内框;
    • 行内元素能排在一行,允许左右有其它元素。

    3. display 属性的影响

    • block,元素生成一个块框;
    • inline,元素产生一个或多个的行内框;
    • inline-block,元素产生一个行内级块框,行内块框的内部会被当作块框来格式化,而此元素本身会被当作行内级框来格式化(这也是为什么会产生 BFC);
    • none,不生成框,另一个 visibility:hidden 则会产生一个不可见的框。

    三、BFC(Block Formatting Context)

    FC 即格式上下文,它定义框内部的元素渲染规则,比较抽象,譬如:

    • 格式上下文像是一个大箱子,里面装有很多元素;
    • 箱子可以隔开里面的元素和外面的元素(所以外部并不会影响 FC 内部的渲染);
    • 内部的规则可以是:如何定位、宽高计算等等;
    • 不同类型的框参与的 FC 类型不同,譬如块级框对应 BFC,行内框对应 IFC。

    在块格式化上下文中,每一个元素左外边与包含块的左边相接触(对于从右到左的格式化,右外边接触右边);即使存在浮动也是如此(所以浮动元素正常会直接贴近它的包含块的左边,与普通元素重合),除非这个元素也创建了一个新的 BFC。

    BFC 特点:

    • 内部 Box 在垂直方向,一个接一个的放置;
    • Box 的垂直方向由 margin 决定,属于同一个 BFC 的两个 Box 间的 margin 会重叠;
    • BFC 区域不会与 FloatBox 重叠;
    • BFC 就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素,反之也如此;
    • 计算 BFC 的高度时,浮动元素也参与计算(不会浮动坍塌)。

    如何触发 BFC?

    • 根元素;
    • float 属性不为 none;
    • position:absolute/fixed;
    • display:inline-block/flex/inline-flex/table/table-cell/table-caption;
    • overflow:hidden。

    注意:display:table 本身不产生 BFC,但是它会产生匿名框(包含 display:table-cell 的框),而这个匿名框产生 BFC。

    JS 引擎解析过程

    前面有提到遇到 JS 脚本时,会等到它的执行,实际上是需要引擎解析的,这里展开描述。

    一、JS 解释阶段

    JS 是解释型语音,所以它无需提前编译,而是由解释器实时运行,引擎对 JS 的处理过程可以简述如下:

    1. 读取代码,进行词法分析(Lexical Analysis),然后将代码分解成词元(token);
    2. 对词元进行语法分析(Parsing),然后将代码整理成语法树(Syntax Tree);
    3. 使用翻译器(Translator),将代码转为字节码(Bytecode);
    4. 使用字节码解释器(Bytecode Interpreter),将字节码转为机器码;
    5. 最终计算机执行的就是机器码。

    现代浏览器一般采用即时编译(JIT-Just In Time Compiler),即字节码只在运行时编译,用到哪一行就编译哪一行,并且把编译结果缓存。

    二、JS 预处理阶段

    在正式执行 JS 前,还会有一个预处理阶段(譬如变量提升、分号补全等),确保 JS 可以正确执行,这里仅提部分:

    1. 分号补全

    JS 执行是需要分号的,但为什么以下语句却可以正常运行呢?

    1
    2
    
    console.log('a');
    console.log('b');
    

    原因就是 JS 解释器有一个 Semicolon Insertion 规则,它会按照一定规则,在适当的位置补充分号。

    2. 变量提升

    一般包括函数提升和变量提升,譬如:

    1
    2
    3
    4
    5
    6
    
    a = 1;
    b();
    function b() {
     console.log('b');
    }
    var a;
    

    经过变量提升后,就变成:

    1
    2
    3
    4
    5
    6
    
    function b() {
     console.log('b');
    }
    var a;
    a = 1;
    b();
    

    三、JS 执行阶段

    此段内容中的图片来源: 深入理解 JavaScript 系列(10):JavaScript 核心

    解释器解释完语法规则后,就开始执行,整个执行流程中大致包含:

    • 执行上下文,执行堆栈概念(如全局上下文、当前活动上下文);
    • VO(变量对象)和 AO(活动对象);
    • 作用域链;
    • this。

    1. 执行上下文简单解释

    1. 浏览器首次载入脚本,它将创建全局执行上下文,并压入执行栈栈顶(不可被弹出);
    2. 每进入其它作用域就创建对应的执行上下文并把它压入执行栈的顶部;
    3. 一旦对应的上下文执行完毕,就从栈顶弹出,并将上下文控制权交给当前的栈;
    4. 这样依次执行(最终都会回到全局执行上下文)。

    如果程序执行完毕,被弹出执行栈,然后又没有被引用(没有形成闭包),那么这个函数中用到的内存就会被垃圾处理器自动回收。

    每一个执行上下文,都有三个重要属性:

    • 变量对象(Variable Object);
    • 作用域链(Scope Chain);
    • this。

    2. 变量对象与活动对象

    只有全局上下文的变量对象允许通过 VO 的属性名称来间接访问(因为在全局上下文里,全局对象自身就是变量对象);AO(Activation Object),当函数被调用者激活,AO 就被创建了。

    • 在函数上下文中:VO === AO;
    • 在全局上下文中:VO === this === global。

    3. 作用域链

    它是执行上下文中的一个属性,原理和原型链很相似,作用很重要,譬如流程简述:在函数上下文中,查找一个变量 foo;如果在函数的 VO 中找到了,就直接使用;否则去它的父级作用域链中找;如果父级中没找到,继续往上找;直到全局上下文中也没找到就报错。

    4. this 指针

    this 是执行上下文环境的一个属性,而不是某个变量对象的属性:

    • this 是没有一个类似搜寻变量的过程;
    • 当代码中使用了 this,this 的值就直接从执行的上下文中获取了,而不会从作用域链中搜寻;
    • this 的值只取决中进入上下文时的情况。

    四、回收机制(Garbage Collecation)

    JS 有垃圾处理器,所以无需手动回收内存;而是由垃圾处理器自动处理,常用的两种垃圾回收规则是:

    • 标记清除;
    • 引用计数。

    JS 引擎基础 GC 方案是 Mark and Sweep(标记清除),简单解释如下:

    • 遍历所有可访问的对象;
    • 回收已不可访问的对象。

    1. GC 的缺陷:

    和其他语言一样,JS 的 GC 策略也无法避免一个问题:垃圾回收时,停止响应其他操作;这是为了安全考虑。而 JS 的 GC 在 100ms 甚至以上,对一般的应用还好,但对于 JS 游戏,动画对连贯性要求比较高的应用,就麻烦了。这就是引擎需要优化的点:避免 GC 造成的长时间停止响应。

    2. GC 的优化策略:分代回收(Generation GC)

    目的是通过区分“临时”与“持久”对象:

    • 多回收“临时对象”区(Young Generation);
    • 少回收“持久对象”区(Tenured Generation);
    • 减少每次需遍历的对象,从而减少每次 GC 的耗时。

    更多参考: V8 内存浅析

    跨域

    譬如发出网络请求时,会用 ajax,如果接口跨域,就会遇到跨域问题,可以参考:

    • ajax 跨域,这应该是最全的解决方案了

    安全

    譬如浏览器在解析 HTML 时,有 XSSAuditor,可以延伸到安全相关领域,可以参考:

    • ajax 请求真的不安全么?谈谈 Web 安全与 ajax 的关系

    参考

    • 从输入 URL 到页面加载的过程?如何由一道题完善自己的前端知识体系


沪ICP备19023445号-2号
友情链接