从输入URL到页面加载的过程所涉及到的知识点

本文讲述了从输入 URL 到页面加载完成所涉及到的部分浏览器知识、网络知识、前端相关知识等,内容参考了若干大佬的文章,若有错误,烦请不吝赐教!

总览图

从输入URL到页面加载的过程

浏览器基础

多进程浏览器

  • 1 个浏览器进程
  • 多个渲染进程(renderer)
  • 多个插件进程
  • 1 个 GPU 进程
  • 1 个网络进程

多进程的好处可以避免单个 page、单个插件 crash 影响整个浏览器,也充分利用多核优势,提高浏览器稳定性,速度,安全性。

进程策略:多个 tab 同域名下可能会合并到一个渲染进程。通过页面右上角的... -> 更多工具 -> 任务管理器打开面板。

进程是系统分配的独立资源,是 CPU 资源分配的基本单位,进程是由一个或者多个线程组成的。
线程是进程的执行流,是 CPU 调度和分派的基本单位,同个进程之中的多个线程之间是共享该进程的资源的。

进程间的交互(网页加载过程中)

  1. 浏览器进程接收到用户输入的 URL 请求,判断是关键词还是 URL(关键词则跳转默认搜索引擎搜索),然后将该 URL 转发给网络进程发起真正的请求(进程间通信 IPC)。
  2. 得到请求来的数据后,网络进程解析响应头数据,并将数据转发给浏览器进程,进而浏览器进程发送 “确认导航” 消息到渲染进程
  3. 渲染进程收到消息后,与网络进程建立数据管道,开始准备接收 HTML 数据;准备就绪后,渲染进程浏览器进程发送 “确认提交” 消息,浏览器开始移除旧文档,更新浏览器进程中的页面状态。
  4. 一旦文档被提交,渲染进程便开始页面解析和子资源加载,页面渲染完成后,渲染进程会发送一个消息给浏览器进程做完成处理。

进程间的交互

  • GUI 渲染线程
    • 负责渲染页面,解析 HTMLCSS 构成 DOM 树等,当页面重绘或者由于某种操作引起回流都会调起该线程
    • JS 引擎线程是互斥的,当 JS 引擎线程在工作的时候,GUI 渲染线程会被挂起,GUI 更新被放入在 JS 任务队列中,等待 JS 引擎线程空闲的时候继续执行
  • JS 引擎线程(如 v8 引擎等)
    • 单线程工作,负责将 JavaScript 翻译成 CPU 指令(机器码
    • GUI 渲染线程互斥,JS 运行耗时过长就会导致页面阻塞
    • 一个浏览器 Tabrenderer 进程)只有一个 js 线程运行
    • 更多见下文 v8 引擎工作原理
  • 事件触发线程
    • 控制事件循环,管理一个任务队列/事件队列,异步任务触发条件达成,将回调事件放到任务队列中
    • 当事件符合触发条件被触发时,该线程会把对应的事件回调函数添加到任务队列的队尾,等待 JS 引擎处理
  • 定时器触发线程
    • setIntervalsetTimeout 所在线程
    • 之所以有单独的线程,是因为 JS 引擎阻塞会导致计时不准确
    • 开启定时器触发线程来计时并触发计时,计时完成后会被添加到任务队列中,等待 JS 引擎处理
  • 异步 http 请求线程
    • 每次 http 请求的时候都会新开启一条请求线程
    • 请求完成有结果了之后,将请求的回调函数添加到任务队列中,等待 JS 引擎处理
  • 合成线程
    • GUI 渲染线程后执行,将 GUI 渲染线程生成的待绘制列表转换为位图
  • IO 线程
    • 用来和其他进程进行通信

浏览器内核主要分为:

  • Webkit内核:苹果基于 KHTML 开发、开源的,用于 Safari,Google Chrome 之前也在使用,由 WebCore 和 JavaScriptCore 组成;
  • Blink内核:是 Webkit 的一个分支,Google 开发,目前应用于 Google Chrome、Edge、Opera 等;

JS 引擎主要分为:

  • SpiderMonkey:第一款 JavaScript 引擎,由 Brendan Eich 开发(也就是 JavaScript 作者)
  • Chakra:微软开发,用于 IT 浏览器
  • JavaScriptCore:WebKit 中的 JavaScript 引擎,Apple 公司开发
  • V8:Google 开发的强大 JavaScript 引擎,也帮助 Chrome 从众多浏览器中脱颖而出

浏览器进程与线程

web workers

  • 原理:JS 引擎向浏览器新申请开一个子线程,与子线程通过 postMessage API 通信,子线程完全受主线程控制
  • 作用:后台运行计算,将结果发到主线程,解决单线程的 JS 引擎进行密集型计算会堵塞页面的问题
  • 子线程不能影响用户界面,即不能操作 dom 等,在一个新的全局上下文
  • 除了 webworker(属于 renderer 进程) 还有 SharedWorker(多个标签页、iframe 共享,不属于某个 renderer 进程,自己就是一个进程),Service WorkersChromeWorker
  • 更多查阅 1,查阅 2

网络基础

用户输入网址,浏览器检查缓存后,发起 DNS 解析查询到对应 IP(应用层),根据 IP 协议找到目标服务器(网络层),通过 Mac 寻址找到服务器硬件接口(数据链路层),然后通过网线 wifi 等向服务器硬件接口传输比特信息(物理层),即开始建立 tcp 连接(传输层),如果是 HTTPS 则先建立 SSL(会话层、表示层),以上建立完成后,开始发送 HTTP 请求(应用层)。

服务端的接收就是反过来的步骤。

七层因特网协议栈(ISO)

  1. 应用层(http,ftp,dns) 这一层为操作系统或网络应用程序提供访问网络服务的接口,如 DNS 解析成 IP 并发送 http 请求
  2. 表示层(SSL 握手协议) 主要处理两个通信系统中交换信息的表示方式,包括数据格式交换,数据加密与解密,数据压缩与终端类型转换等
  3. 会话层(SSL 记录协议) 这一层管理主机之间的会话进程,即负责建立、管理、终止进程之间的会话,如控制登陆和注销过程 (QoS)
  4. 传输层(tcp,udp) 建立 tcp/udp 连接,数据的单位称为数据段(segment)(四层交换机)
  5. 网络层(IP,ARP) IP 寻址,数据的单位称为数据包(packet)(路由器、三层交换机)
  6. 数据链路层(PPP) 将 bit 流封装成 frame 帧(网桥、二层交换机,以太网)
  7. 物理层(传输 bit 流) 物理传输(传输通过双绞线,电磁波,光纤,中继器,集线器,网线接口等各种介质)

五层因特网协议栈

  • ip 寻址算法包括:RIP 协议(IP 路由跳转的次数最小,适合小型网络),OSPF 协议(IP 路由跳转的速度最快,适合大型网络)。
  • ARP 协议 是一个通过解析 IP 地址来找寻 Mac 地址的协议,IP 地址转换成 Mac 地址后,就能进行以太网数据传输了。
  • 以太网属于数据链路层,它主要负责相邻设备的通信。原理是通过查询交换机 Mac 表,找到通信双方的物理接口,进而开始通信。

DNS

URL 一般包括几大部分:

  • protocol,协议头,譬如有 http,ftp 等
  • host,主机域名或 IP 地址
  • port,端口号
  • path,目录路径
  • query,即查询参数
  • fragment,即#后的 hash 值,一般用来定位到某个位置

URI 是统一资源标识符,而 URL 是统一资源定位符。每个 URL 都是 URI,但不一定每个 URI 都是 URL。 如 URImailto:cay@horstman.com

如果输入的是域名,需要进行 DNS(Domain Name System,域名系统) 解析成 IP,即域名解析DNS 是一个将域名和 IP 地址相互映射的一个分布式数据库。根据以下优先级查找对应的 IP 地址:

  1. 浏览器缓存
  2. 本机缓存(host 文件等)
  3. 本地域名解析服务器(电信,联通等运营商)
  4. DNS 根域名解析服务器、DNS 二根域名解析服务器

如果请求的是静态资源,那么流量有可能到达 CDN 服务器;如果请求的是动态资源,那么情况更加复杂,流量可能依次经过代理/网关、Web 服务器、应用服务器、数据库.

dns 解析是很耗时的,因此如果解析域名过多,会让首屏加载变得过慢,可以考虑dns-prefetch优化。减少域名 DNS 解析时间将网页加载速度提升新层次

TCP

有了 IP 地址之后,在 http 请求之前,客户端和服务器端先建立 TCP 连接。TCP 是一种全双工的、面向连接的、可靠的、基于字节流的传输层通信协议。

名词解释

  • SYN(synchronous 建立联机)同步序列号
  • ACK(acknowledgement 确认)应答码
  • PSH(push 传送)
  • FIN(finish 结束)
  • RST(reset 重置)
  • URG(urgent 紧急)
  • Sequence number(seq 顺序号码) 对方上次的 ack(首次发送时 seq 为系统随机生成)
  • Acknowledge number(ack 确认号码)对方的 seq+1(无数据传输时) 或者 seq+L(报文数据的长度 L)

三次握手的步骤

三次握手的目的是连接服务器指定端口,建立 TCP 连接,并同步连接双方的序列号和确认号,交换 TCP 窗口大小信息。在 socket 编程中,客户端执行 connect() 时,将触发三次握手。

  1. 客户端发送SYN包(SYN=1,seq=x,x 为随机数)的数据包到服务器,并进入SYN_SEND状态;
  2. 服务器收到SYN包后,发送一个ACK包(ACK number=x+1,ACK=1)确认,同时自己也发送一个SYN包(SYN=1,seq=y),即SYN+ACK包,此时服务器进入SYN_RCVD状态;
  3. 客户端收到SYN+ACK包后,发送一个ACK包(ACK number=y+1,ACK=1)确认,发送完毕客户端进入ESTABLISHED状态,服务端收到包后也进入ESTABLISHED状态,完成三次握手

建立连接成功后,接下来就正式传输数据。

三次握手

四次挥手的步骤

客户端或服务器均可主动发起挥手动作,在 socket 编程中,任何一方执行 close() 操作即可产生挥手操作。假设客户端想关闭连接:

  1. 客户端向服务器发送 FIN=1,seq=x 包,表示客户端主动要关闭连接,然后进入 FIN_WAIT_1 状态。(此后客户端不能再向服务器发送数据,但能读取数据。)
  2. 服务器收到 FIN 包后,向客户端发送 ACK=1,ACKnum=x+1 确认包,然后进入 CLOSE_WAIT 状态。(此后服务器不能再读取数据,但可以继续向客户端发送数据。同时客户端收到这个确认包后,进入 FIN_WAIT_2 状态)。
  3. 服务器完成数据的发送后,向客户端发送 FIN=1,seq=y 包,然后进入 LAST_ACK 状态,等待客户端返回最后一个 ACK 包。此后服务器既不能读取数据,也不能发送数据。
  4. 客户端收到 FIN 包后,向服务器发送 ACK=1,ACKnum=y+1 确认包,然后进入 TIME_WAIT 状态,接着等待足够长的时间(2MSL),没有收到服务器端的 ACK,认为服务器端已经正常关闭连接,于是自己关闭链接进入 CLOSED 状态,释放网络资源。

注:2,3 次挥手不会一起发送,当服务器收到 FIN 报文时,很可能并不会立即关闭 SOCKET,所以只能先回复一个 ACK 报文,告诉 Client端,”你发的 FIN报文 我收到了”。只有等到我服务器所有的报文都发送完了,我才能发送 FIN报文,因此不能一起发送。

四次挥手

SYN 攻击

攻击客户端在短时间内伪造大量不存在的 IP 地址,向服务器不断地发送 SYN 包,服务器回复确认包,并等待客户的确认。由于源地址是不存在的,服务器需要不断的重发直至超时,这些伪造的 SYN 包将长时间占用未连接队列,正常的 SYN 请求被丢弃,导致目标系统运行缓慢,严重者会引起网络堵塞甚至系统瘫痪。SYN 攻击是一种典型的 DoS/DDoS 攻击。

防御:

  • 缩短超时(SYN Timeout)时间
  • 增加最大半连接数
  • 过滤网关防护
  • SYN cookies 技术

TCP KeepAlive

隔一段时间给连接对端发送一个探测包,如果收到对方回应的 ACK,则认为连接还是存活的,在超过一定重试次数之后还是没有收到对方的回应,则丢弃该 TCP 连接。

tcp/ip 的并发限制

  • HTTP/1.0 中,一个服务器在发送完一个 HTTP 响应后,会断开 TCP 链接。但是这样每次请求都会重新建立和断开 TCP 连接,代价过大。
  • 所以 HTTP/1.1 就把 Connection 头写进标准(Connection: keep-alive),并且默认开启持久连接,除非请求中写明 Connection: close,那么浏览器和服务器之间是会维持一段时间的 TCP 连接,不会一个请求结束就断掉,这样 SSL 的开销也可以避免,故刷新页面也不会重新建立 TCP 和 SSL。(在 chrome 浏览器里通过 network 标签 -> connection ID表示TCP连接的重用。)
  • HTTP/1.1中,单个 TCP连接 在同一时刻只能处理一个请求,顺序处理多个请求; 在 HTTP2 中由于 Multiplexing 特点的存在,多个 HTTP 请求 可以在同一个 TCP 连接 中并行进行。
  • 浏览器对同一域名下并发的 TCP连接 是有限制的(2-10 个不等),Chrome 最多允许对同一个 Host 建立六个 TCP连接

get 和 post 的区别

  • get 安全幂等,从服务器获取资源;postURI 指定的资源提交数据,数据就放在报文的 body 里;
  • get 请求时,浏览器会把 headersdata 一起发送出去,服务器响应 200 返回数据(发送一个 tcp 数据包);
  • post 请求时,浏览器先发送headers,服务器响应 100 continue, 浏览器再发送 data,服务器响应 200 返回数据(发送两个 tcp 数据包)。

TCP/UDP 的区别

  • TCP 是面向连接的,UDP 是面向无连接的。TCP 在通信之前必须通过三次握手机制与对方建立连接,而 UDP 通信不必与对方建立连接,不管对方的状态就直接把数据发送给对方
  • TCP 连接过程耗时,UDP 不耗时
  • TCP 连接过程中出现的延时增加了被攻击的可能,安全性不高,而 UDP 不需要连接,安全性较高
  • TCP 是可靠的,保证数据传输的正确性,不易丢包;UDP 是不可靠的,易丢包
  • TCP 传输速率较慢,实时性差,udp 传输速率较快。tcp 建立连接需要耗时,并且 tcp 首部信息太多,每次传输的有用信息较少,实时性差
  • TCP 是流模式,udp 是数据包模式。tcp 只要不超过缓冲区的大小就可以连续发送数据到缓冲区上,接收端只要缓冲区上有数据就可以读取,可以一次读取多个数据包,而 udp 一次只能读取一个数据包,数据包之间独立

TCP 可靠性的六大手段

  • 顺序编号:tcp 在传输文件的时候,会将文件拆分为多个 tcp 数据包(http 请求消息太长),每个装满的数据包大小大约在 1k 左右,tcp 协议为保证可靠传输,会将这些数据包顺序编号
  • 确认机制:当数据包成功的被发送方发送给接收方,接收方会根据 tcp 协议反馈给发送方一个成功接收的 ACK 信号,信号中包含了当前包的序号(停止等待协议 ARQ
  • 超时重传:当发送方发送数据包给接收方时,会为每一个数据包设置一个定时器,当在设定的时间内,发送方仍没有收到接收方的 ACK 信号,会再次发送该数据包,直到收到接收方的 ACK 信号或者连接已断开(停止等待协议 ARQ
  • 校验信息:tcp 首部校验信息较多,udp 首部校验信息较少
  • 流量控制:如果发送者发送数据过快,接收者来不及接收,那么就会有分组丢失。为了避免分组丢失,控制发送者的发送速度,使得接收者来得及接收,这就是流量控制。由滑动窗口协议(连续 ARQ 协议)实现。滑动窗口协议既保证了分组无差错、有序接收,也实现了流量控制。主要的方式就是接收方返回的 ACK 中会包含自己的接收窗口的大小,并且利用大小来控制发送方的数据发送
  • 拥塞控制:拥塞控制是作用于网络的,它是防止过多的数据注入到网络中,避免出现网络负载过大的情况;常用的方法就是:慢开始、拥塞避免;快重传、快恢复

HTTP

TCP 三次握手结束后,开始发送 HTTP(超文本传输协议,HyperText Transfer Protocol) 请求报文。

  • HTTP 最凸出的优点是 简单、灵活和易于扩展、应用广泛和跨平台
  • HTTP 最大双刃剑是 无状态、明文传输
  • HTTP 最大缺点是 不安全

HTTP 工作流程

  1. 地址解析
  2. 封装 HTTP 请求数据包
  3. 封装成 TCP 包,建立 TCP 连接(TCP 的三次握手)
  4. 客户机发送请求命令
  5. 服务器响应
  6. 服务器关闭 TCP 连接

客户机会将请求封装成 http 数据包–>封装成 Tcp 数据包–>封装成 Ip 数据包—>封装成数据帧—>硬件将帧数据转换成 bit 流(二进制数据)–>最后通过物理硬件(网卡芯片)发送到指定地点。

服务器硬件首先收到 bit 流,然后转换为数据帧,然后转换成 ip 数据包。于是通过 ip 协议解析 Ip 数据包,然后又发现里面是 tcp 数据包,就通过 tcp 协议解析 Tcp 数据包,接着发现是 http 数据包通过 http 协议再解析 http 数据包得到数据。

HTTP 报文结构

报文一般包括了:通用头部(General)请求/响应头部请求/响应体

通用头部
  • Request Url: 请求的 web 服务器地址
  • Request Method: 请求方式(Get、POST、OPTIONS、PUT、HEAD、DELETE、CONNECT、TRACE)
  • Status Code: 请求的返回状态码,如 200 代表成功
  • Remote Address: 请求的远程服务器地址(会转为 IP)
  • Referrer-Policy: 控制请求头中 referrer 的内容 查阅

在跨域拒绝时,可能是methodoptions,状态码为404/405等(当然,实际上可能的组合有很多)

其中,Method的话一般分为两批次:

  • HTTP1.0定义了三种请求方法:GET, POST 和 HEAD方法。以及几种Additional Request Methods:PUT、DELETE、LINK、UNLINK
  • HTTP1.1定义了八种请求方法:GET、POST、HEAD、OPTIONS, PUT, DELETE, TRACE 和 CONNECT 方法。

列举下状态码不同范围状态的意义,更多查阅

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

常用的请求头部(部分):

  • Accept: 接收类型,表示浏览器支持的 MIME 类型,对应 Content-Type
  • Accept-Encoding:浏览器支持的压缩类型,如 gzip 等,超出类型不能接收
  • Content-Type:客户端发送出去实体内容的类型
  • Cookie: 有 cookie 并且同域访问时会自动带上
  • Connection: 当浏览器与服务器通信时对于长连接如何进行处理,如 keep-alive
  • Host:请求的服务器 URL
  • Origin:最初的请求是从哪里发起的(只会精确到端口),Origin 比 Referer 更尊重隐私
  • Referer:该页面的来源 URL(适用于所有类型的请求,会精确到详细页面地址,csrf 拦截常用到这个字段)
  • User-Agent:用户客户端的一些必要信息,如 UA 头部等
  • If-Modified-Since:http1.0 弱缓存,对应 Last-Modified
  • Expires:http1.0 强缓存、
  • If-None-Match:http1.1 弱缓存,对应 ETag
  • Cache-Control: http1.1 强缓存

常用的响应头部(部分):

  • Access-Control-Allow-Headers: 服务器端允许的请求 Headers
  • Access-Control-Allow-Methods: 服务器端允许的请求方法
  • Access-Control-Allow-Origin: 服务器端允许的请求 Origin 头部(譬如为*)
  • Content-Type:服务端返回的实体内容的类型
  • Content-Length:表明本次回应的数据长度
  • Content-Encoding:表示服务器返回的数据使用了什么压缩格式
  • Date:数据从服务器发送的时间
  • Set-Cookie:设置和页面关联的 cookie,服务器通过这个头部把 cookie 传给客户端
  • Keep-Alive:如果客户端有 keep-alive,服务端也会有响应(如 timeout=38)
  • Server:服务器的一些相关信息
  • Cache-Control:告诉浏览器或其他客户,什么环境可以安全的缓存文档
  • Last-Modified:请求资源的最后修改时间
  • Expires:应该在什么时候认为文档已经过期,从而不再缓存它
  • ETag:请求变量的实体标签的当前值

一般来说,请求头部和响应头部是匹配分析的。

  • 请求头部的Accept要和响应头部的Content-Type匹配,否则会报错
  • 跨域请求时,请求头部的Origin要匹配响应头部的Access-Control-Allow-Origin,否则会报跨域错误
  • 在使用缓存时,请求头部的If-Modified-Since、If-None-Match分别和响应头部的Last-Modified、ETag对应

http报文结构

请求/响应实体
  • 请求实体中会将一些需要的参数都放入进入(用于 post 请求)如实体中可以放参数的序列化形式(a=1&b=2 这种),或者直接放表单对象(Form Data 对象,上传时可以夹杂参数以及文件)等等
  • 响应实体中就是放服务端需要传给客户端的内容,一般现在的接口请求时,实体中就是对于的信息的 json 格式,而像页面请求这种,里面就是直接放了一个 html 字符串,然后浏览器自己解析并渲染。

Form Data 与 Request Payload

post/put 提交中:

  • Content-Type: application/json 时,为 Request Payload (json)
  • Content-Type: application/x-www-form-urlencodedmultipart/form-data 时,为 Form Datastring,格式为 key1=value1&key2=value2,类似 GET 请求的 QueryString 格式)

他们只是因为 Content-Type 设置的不同,并不是数据提交方式的不同,这两种提交都会将数据放在 message-body 中。但是 chrome 浏览器的开发者工具会根据这个 ContentType 区分显示方式。

CRLF

CRLF(Carriage-Return Line-Feed),意思是回车换行,一般作为分隔符存在,请求头和实体消息之间有一个 CRLF 分隔,响应头部和响应实体之间用一个 CRLF 分隔

1
2
3
CRLF->Windows-style
LF->Unix Style
CR->Mac Style

HTTP 1.1 性能瓶颈

  • 请求 / 响应头部(Header)未经压缩就发送,首部信息越多延迟越大。只能压缩 Body 的部分;
  • 发送冗长的首部。每次互相发送相同的首部造成的浪费较多;
  • 串行的文件传输:服务器是按请求的顺序响应的,如果服务器响应慢,会招致客户端一直请求不到数据,也就是队头阻塞;
  • 没有请求优先级控制;
  • 请求只能从客户端开始,服务器只能被动响应。

HTTP 与 TCP/IP 区别

  • TPC/IP 协议是传输层协议,主要解决数据如何在网络中传输
  • HTTP 是应用层协议,主要解决如何包装数据
  • WEB 使用 HTTP 协议作应用层协议,以封装 HTTP 文本信息,然后使用 TCP/IP 做传输层协议将它发到网络上。

长连接与短连接

tcp/ip层面:

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

http 层面:

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

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

HTTP 2.0

  • 多路复用(代替了 HTTP1.x 的序列和阻塞机制,同域名下所有通信都在单个 TCP 连接上完成,消除了因多个 TCP 连接而带来的延时和内存消耗;并且单个连接上可以并行交错请求和响应,之间互不干扰。很多http1.1中的优化方案就无需用到了(譬如打包成精灵图,静态资源多域名拆分等))
  • 首部压缩http头部压缩(HPACK 算法),减少体积)
  • 二进制分帧(在应用层(HTTP/2)跟传输层(TCP or UDP)之间增加了一个二进制分帧层,改进传输性能,实现低延迟和高吞吐量)
  • 服务器端推送(也称为缓存推送 Push Cache,服务端可以对客户端的一个请求发出多个响应,可以主动通知客户端,当一个客户端请求资源 X,而服务器知道它很可能也需要资源 Z 的情况下,服务器可以在客户端发送请求前,主动将资源 Z 推送给客户端)
  • 请求优先级(如果数据流被赋予了优先级,它就会基于这个优先级来处理,由服务器决定需要多少资源来处理该请求。)

HTTP 3.0

http2 多路复用的关系,在出现丢包的情况下,整个 TCP 都要开始等待重传,也就导致了后面的所有数据都被阻塞,因此表现反而不如 http1,所以 Google 就更起炉灶搞了一个基于 UDP 协议的 QUIC 协议,即 http3

QUIC 新功能包括:

  • 0-RTT:类似 TCP 快速打开的技术,缓存当前会话的上下文,在下次恢复会话的时候,只需要将之前的缓存传递给服务端验证通过就可以进行传输了。0RTT 建连可以说是 QUIC 相比 HTTP2 最大的性能优势。
  • 真·多路复用:QUIC 是基于 UDP 的,一个连接上的多个 stream 之间没有依赖,不存在 TCP 队头阻塞。另外 QUIC 在移动端的表现也会比 TCP 好。因为 TCP 是基于 IP 和端口去识别连接的,这种方式在多变的移动端网络环境下是很脆弱的。但是 QUIC 是通过 ID 的方式去识别一个连接,不管你网络环境如何变化,只要 ID 不变,就能迅速重连上。
  • 加密认证的报文:除了个别报文比如 PUBLIC_RESET 和 CHLO,所有报文头部都是经过认证的,报文 Body 都是经过加密的。
  • 向前纠错机制(FEC):每个数据包除了它本身的内容之外,还包括了部分其他数据包的数据,因此少量的丢包可以通过其他包的冗余数据直接组装而无需重传。向前纠错牺牲了每个数据包可以发送数据的上限,但是减少了因为丢包导致的数据重传,因为数据重传将会消耗更多的时间。

HTTPS

httpshttp的区别就是:在请求前,会建立ssl链接,确保接下来的通信都是加密的,无法被轻易截取分析。

HTTPS 原理(SSL 四次握手)

一般来说,主要关注的就是 SSL/TLS 的握手流程:

  1. ClientHello

    浏览器发起 https 请求建立 SSL链接(服务器的 443 端口),并向服务端发送以下信息(第一次 HTTP 请求,即明文传输):

    • 随机数 Client random,后面用于生成 会话对称加密密钥
    • 客户端支持的加密方法,比如 RSA 加密
    • 客户端支持的 SSL/TLS 协议版本,如 TLS 1.2 版本
  2. SeverHello

    服务器收到客户端请求后,向客户端发出响应,向客户端发送以下信息(明文传输):

    • 随机数 Server random,后面用于生成 会话对称加密密钥
    • 确认的 加密算法Hash算法
    • 确认 SSL/ TLS 协议版本,如果浏览器不支持,则关闭加密通信
    • 服务器的数字证书(证书里包含了网站地址,非对称加密的公钥,以及证书颁发机构等信息)
  3. 浏览器回应

    TLS 来验证证书的合法性(通过浏览器或者操作系统中的 CA 公钥验证颁发机构是否合法,证书中包含的网址是否和正在访问的一样,过期时间等),如果证书信任则浏览器会显示一个小锁头,否则会弹出一个警告框提示证书存在问题。

    客户端接收证书后(不管信不信任),会从数字证书中取出服务器的公钥,然后使用它加密报文,向服务器发送如下信息(第二次 HTTP 请求):

    • 新的随机数 Premaster secret,该随机数会被服务器公钥加密
    • 加密通信算法改变通知,表示随后的信息都将用「会话秘钥」加密通信。
    • 客户端握手结束通知,表示客户端的握手阶段已经结束。这一项同时把之前所有内容的发生的数据做个摘要,用来供服务端校验。

    利用 Client randomServer randomPremaster secret 通过双方协商的加密算法各自生成本次通信的 会话对称加密密钥

  4. 服务器的最后回应

    服务器收到客户端的第三个随机数 Premaster secret 之后,通过协商的加密算法计算出本次通信的 会话对称加密密钥。使用该密钥解密浏览器发来的握手消息,并验证Hash是否与浏览器发来的一致。然后向客户端发生最后的信息:

    • 加密通信算法改变通知,表示随后的信息都将用会话对称加密密钥加密通信。
    • 服务器握手结束通知,表示服务器的握手阶段已经结束。这一项同时把之前所有内容的发生的数据做个摘要,用来供客户端校验。
  5. 浏览器解密并计算握手消息的 HASH,如果与服务端发来的 HASH一致,此时握手过程结束。接下来,客户端与服务器进入加密通信,就完全是使用普通的 HTTP 协议,只不过用会话对称加密密钥加密内容。

HTTPS 加密是在传输层

https 报文在被包装成 tcp 报文的时候完成加密的过程,无论是 httpsheader 域也好,body 域也罢都是会被加密的。

当使用 tcpdump 或者 wireshark 之类的 tcp 层工具抓包,获取是加密的内容,而如果用应用层抓包,使用 Charels(Mac)、Fildder(Windows) 抓包工具,那当然看到是明文的。

加密算法,信息摘要,数字签名,数字证书,CA
  • 对称加密算法:AES,RC4,3DES
  • 非对称加密算法:RSA,DSA/DSS
  • 摘要算法(哈希 hash 算法、散列算法):MD5,SHA1,SHA256,加盐 salt 提升复杂度
  • 信息摘要:通过信息摘要算法(HASH),将原信息摘要为一个固定长度的摘要
  • 数字签名:信息摘要被私钥加密后的密文
  • 数字证书:可以简单理解为 被 CA 承认且无法篡改的公钥,可用于验证网站是否可信(针对 HTTPS)、验证某文件是否可信(是否被篡改)等,也可以用一个证书来证明另一个证书是真实可信,最顶级的证书称为根证书。除了根证书(自己证明自己是可靠),其它证书都要依靠上一级的证书,来证明自己。
  • CA:Certificate Authority 签发证书的权威机构
  • CA``:CACA,可以签发 CA 的证书
  • 根证书:根 CA 的自签名证书,内置在操作系统和浏览器中
HTTPS 和 HTTP 的区别
  1. http 的信息是明文传输,连接简单无状态,https 则是安全性的加密传输。
  2. http 是直接与 TCP 进行数据传输,https 是经过一层 SSL(OSI 会话层),端口前者是 80,后者是 443
  3. https 协议握手阶段比较费时,需要到 ca 申请证书或自制证书,会使页面的加载时间延长近 50%,增加 10% 到 20% 的耗电。
  4. https 连接缓存不如 http 高效,会增加数据开销和功耗,甚至已有的安全措施也会因此而受到影响。
  5. 谷歌曾在 2014 年 8 月份调整搜索引擎算法,并称“比起同等 HTTP 网站,采用 HTTPS 加密的网站在搜索结果中的排名将会更高”。

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

场景如下:

在登陆页面,用户登陆了,此时服务端会生成一个 session,session 中有对于用户的信息(如用户名、密码等),然后会有一个 sessionid(相当于是服务端的这个 session 对应的 key)。然后服务端在返回数据时,设置浏览器本地 cookie 值为:jsessionid=xxx。以后访问同域名下的页面时,自动带上 cookie 用于检验在有效时间内无需二次登陆。

cookie 浅析

GZIP 压缩

首先,明确gzip是一种压缩格式,需要浏览器支持才有效(不过一般现在浏览器都支持), 而且gzip压缩效率很好(高达 70%左右),一般是由apache、tomcatweb服务器开启,一般只需要在服务器上开启了gzip压缩,然后之后的请求就都是基于gzip压缩格式的,当然服务器除了gzip外,也还会有其它压缩格式(如 deflate,没有 gzip 高效,且不流行)。

后台处理

负载均衡

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

后台的处理

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

前端缓存

对于一个数据请求来说,可以分为发起网络请求、后端处理、浏览器响应三个步骤。浏览器缓存可以帮助我们在第一和第三步骤中优化性能。

  • 浏览器每次发起请求,都会先在浏览器缓存中查找该请求的结果以及缓存标识
  • 浏览器每次拿到返回的请求结果都会将该结果和缓存标识存入浏览器缓存中

缓存位置优先级

  1. Service Worker
  2. Memory Cache
  3. Disk Cache(HTTP cache)
  4. Push Cache

都未取到则进行正式的网络请求请求最新数据。

Service Worker

Service Worker 是运行在浏览器背后的独立线程,传输协议必须为 HTTPS。他与浏览器其他内建的缓存机制不同,它可以让我们自由控制缓存哪些文件、如何匹配缓存、如何读取缓存,并且缓存是持续性的。

实现步骤:

  1. 注册 Service Worker,然后监听到 install 事件以后就可以缓存需要的文件
  2. 下次用户访问可以通过拦截请求的方式查询是否存在缓存,存在则直接读取缓存文件
  3. 如果没有命中缓存,则调用 fetch 函数按缓存优先级获取数据,无论此时从哪里来的数据 netWorksize 都将显示为 Service Worker

Memory Cache

  • 内存中的缓存,几乎所有的网络请求资源都会被浏览器自动加入到 memory cache 中,短期存储 (preload)
  • 浏览器的 TAB 关闭后该次浏览的 memory cache 便失效
  • 无视 HTTP 头信息,从 memory cache 获取缓存内容时,浏览器会忽视例如 max-age=0, no-cache 等头部配置
  • Cache-control:no-store 是例外,可以让 memory cache 失效

Disk Cache

Disk Cache (HTTP cache) 也就是存储在硬盘中的缓存,读取速度慢点,但是什么都能存储到磁盘中,比之 Memory Cache 胜在容量和存储时效性上。

强缓存

强制缓存(200 from cache),当客户端请求后,会先访问缓存数据库看缓存是否存在。如果存在则直接返回;不存在则请求真的服务器,响应后再写入缓存数据库。强制缓存直接减少请求数,是提升最大的缓存策略。

  • http1.0: Expires 表示缓存到期时间(绝对时间),如:Expires: Thu, 10 Nov 2017 08:45:11 GMT (缺点:写法复杂;本地时间不准确会出现错误)
  • http1.1: Cache-control 表示缓存的最大有效时间(相对时间),如:Cache-control: max-age=2592000 (其他值:no-cache,no-store,public,private 等)

注:http1.0 若想实现 no-cache,使用 Pragma: no-cache,或者 HTMLmeta 标签:<meta http-equiv="Pragma" content="no-cache" />

频繁变动的资源: Cache-Control: no-cache
不常变化的资源: Cache-Control: max-age=31536000

弱缓存

协商缓存(304 Not Modified)、对比缓存,强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程。命中返回 304 Not Modified, 不命中返回 200,新资源和缓存标识存入浏览器缓存。在请求数上和没有缓存是一致的,只在响应体体积上有大幅度节省(304 时,Network size 指服务端通信报文的大小)。

  • http1.0: Last-Modified 和 If-Modified-Since 根据文件修改时间判断是否过期,精确到秒。如:Last-Modified: Mon, 10 Nov 2018 09:10:11 GMT
  • http1.1: ETag 和 If-None-Match 根据文件内容判断是否过期,如:etag: "FoWFkJxGtx5Edfyku7luLdv_wucA"

Push Cache

Push Cache(推送缓存)是 HTTP/2 中的内容,当以上三种缓存都没有命中时,它才会被使用。它只在会话(Session)中存在,一旦会话结束就被释放,并且缓存时间也很短暂,在 Chrome 浏览器中只有 5 分钟左右,同时它也并非严格执行 HTTP 头中的缓存指令。

缓存过程

  1. 调用 Service Workerfetch 事件响应
  2. 查看 memory cache
  3. 查看 disk cache
    1. 如果有强制缓存且未失效,则使用强制缓存,不请求服务器。这时的状态码全部是 200
    2. 如果有强制缓存但已失效,使用对比缓存,比较后确定 304 还是 200
  4. 发送网络请求,等待网络响应
  5. 把响应内容存入 disk cache (如果 HTTP 头信息配置可以存的话)
  6. 把响应内容的引用存入 memory cache (无视 HTTP 头信息的配置)
  7. 把响应内容存入 Service WorkerCache Storage (如果 Service Worker 的脚本调用了 cache.put())

浏览器行为

  • 打开网页,地址栏输入地址: 查找 disk cache 中是否有匹配。如有则使用;如没有则发送网络请求。
  • 普通刷新 (F5):因为 TAB 并没有关闭,因此 memory cache 是可用的,会被优先使用(如果匹配的话)。其次才是 disk cache
  • 强制刷新 (Ctrl + F5):浏览器不使用缓存,因此发送的请求头部均带有 Cache-control: no-cache (为了兼容,还带了 Pragma: no-cache),服务器直接返回 200 和最新内容。

页面渲染流程

浏览器获取到 html 等资源后,然后解析,渲染,通过 chrome控制台->Performance->EventLog 可看到解析流程。

流程简述

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

  1. DOM)解析 HTML 内容为 DOM
  2. Recalculate Style)将 CSS转换为 styleSheets,并计算出 DOM 节点的样式
  3. Layout Tree)生成布居树(渲染树),并计算元素的布局信息
  4. Layer Tree)对布局树进行分层(渲染层),生成图层树
  5. Paint)为每个图层生成绘制列表,并将其提交到合成线程
  6. Tiles/raster)将图层分成图块,并在光栅化线程池中将图块转换成位图
  7. Composite Layers)合并图层形成图片,存入 GPU 内存中
  8. DrawQuad)浏览器进程读取 GPU 内存,显示网页

前五步在渲染线程,6,7 步在合成线程,最后一步在浏览器进程

页面渲染流程

HTML 解析,构建 DOM 树

即:字节 -> 字符 -> 令牌 -> 节点对象 -> 对象模型

  • 解码:浏览器将获得的 HTML 内容(Bytes)基于他的编码转换为单个字符
  • 令牌化:浏览器按照 HTML 规范标准将这些字符转换为不同的标记 token,每个 token 都有自己独特的含义以及规则集
  • 词法分析:生成的令牌转换成定义其属性和规则的对象(节点对象)
  • DOM 构建:DOM 树构建完成,整个对象集合就像是一棵树形结构

DOM 树构建完成后,并且 html 所引用的内联外链 js 的同步代码都执行完毕后(不包括样式表,图片)触发 DomContentLoaded 事件。与 async 加载的脚本顺序无关,但一定加载在 defer 脚本执行完成之后。

jQuery $(document).ready(()=>{}); 即监听的 DOMContentLoaded 事件。

解析 CSS,进行样式计算

  • 格式化样式表:类似 HTML 解析,即:Bytes → characters → tokens → nodes → CSSOM,得到 styleSheets(CSSOM)。可通过 document.styleSheets 查看结果
  • 标准化样式表:如 em->px,red->rgba(255,0,0,0),bold->700
  • 计算每个 DOM 节点具体样式:通过 继承层叠,完成了 DOM 节点中每个元素的具体样式的计算。可通过 window.getComputedStyle 查看结果

生成布居树 Layout Tree(渲染树 Render Tree)

  • 流览器布局系统需要额外去构建一棵只包含可见元素布局树 Layout Tree(在 DOM 树上不可见的、head、meta、display:none,最后都不会出现在布局树上)
  • 计算布局树节点的坐标位置

生成布居树

分层

  • 生成图层树(Layer Tree

    DOM 树中每个节点都会对应一个 LayoutObject,当他们的 LayoutObject 处于相同的坐标空间时,就会形成一个渲染层(Render Layers)

    Chrome -> More Tools -> Layer 查看层信息

    布居树与图层树

  • 渲染引擎会为特定的节点创建单独的图层(复合层/合成层

    某些特殊的渲染层会被认为是合成层(Compositing Layers),合成层拥有单独的 图形层(GraphicsLayer),会单独分配资源,所以又称为开启 GPU 硬件加速。而其他不是合成层的渲染层,则和其第一个拥有 GraphicsLayer 父层公用一个。

    • (显式合成)拥有层叠上下文属性的元素会被提升为单独一层:HTML 根元素;opacity;isolation;will-change;video,canvas,iframe;position:fixed;3D transforms:translate3d、translateZ 等
    • (显式合成)需要裁剪(clip)的地方也会创建图层:比如一个标签很小,50*50 像素,你在里面放了非常多的文字,那么超出的文字部分就需要被剪裁;如果出现了滚动条,那么滚动条也会被单独提升为一个图层
    • (隐式合成)z-index 比较低的节点会提升为一个单独的图层,那么层叠等级比它高的节点都会成为一个独立的图层

    Chrome -> More Tools -> Rendering -> Layer borders:黄色的就是复合图层信息。

合成层的优劣及建议:

  • 优:1.比 cpu 处理快(即GPU 硬件加速) 2.repaint 不会影响其他层 3.可以直接合成
  • 劣:大量的合成层会使传输到 GPU 变慢,并且占用 GPU 和内存资源,还会出现层爆炸
  • 建议:动画使用 transform 实现; 减少隐式合成; 减小合成层的尺寸

图层绘制

完成了图层的构建,渲染引擎会把一个复杂的图层拆分为很小的绘制指令,然后再按照这些指令的顺序组成一个待绘制列表。

Chrome -> More Tools -> Layer-profiler 可以看到绘制列表,右侧即为列表绘制过程。

栅格化

栅格化/光栅化(raster):图块生成位图。

当图层的绘制列表准备好之后,渲染线程会把该绘制列表提交(commit)给合成线程。合成线程会将图层划分为图块 tile,按照视口附近的图块来优先生成位图。渲染进程维护了一个栅格化的线程池,所有的图块栅格化都是在线程池内执行(这指在没有启用硬件加速的浏览器,即CPU 软件渲染)。

快速栅格化、GPU 栅格化:通常,栅格化过程都会使用 GPU 来加速生成,渲染进程中通过 GraphicsContextGraphicsLayer 都有一个 GraphicsContext)把生成位图的指令通过共享内存作为纹理上传到 GPU 交由 GPU 合成,生成的位图被保存在 GPU 内存中。属于GPU 硬件加速渲染.更多介绍

栅格化

合成

什么是合成?合成是一种将页面分成若干层,然后分别对它们进行光栅化,最后在一个单独的线程 - 合成线程(compositor thread)里面合并成一个页面的技术。当用户滚动页面时,由于页面各个层都已经被光栅化了,浏览器需要做的只是合成一个新的帧发送给 GPU 来展示滚动后的效果罢了。页面的动画效果实现也是类似,将页面上的层进行移动并构建出一个新的帧即可。

当图层上面的图块都被栅格化后,合成线程会收集图块上面叫做绘画四边形(draw quads)的信息来构建一个合成帧(compositor frame)

  • 绘画四边形:包含图块在内存的位置以及图层合成后图块在页面的位置之类的信息
  • 合成帧:代表页面一个帧的内容的绘制四边形集合(60fps 指的就是这个)

显示

合成线程通过 IPC 向浏览器进程提交一个渲染帧。浏览器进程里viz 组件用来接收合成线程发过来的 DrawQuad 命令,然后根据 DrawQuad 命令,合成帧都会被发送给 GPU 进行聚合并生成最终完整的合成表面,最后再将内存发送给显卡,最后显示在屏幕上。

显卡:屏幕刷新频率为 60 帧,一张图片停留的时间约为 16.7 ms。而每次更新的图片都来自显卡的前缓冲区。而显卡接收到浏览器进程传来的页面后,会合成相应的图像,并将图像保存到后缓冲区,然后系统自动将前缓冲区和后缓冲区对换位置,如此循环更新。如果某个动画逻辑占用大量内存,浏览器生成图像的时候会变慢,图像传送给显卡就会不及时,而显示器还是以不变的频率刷新,因此会出现卡顿,也就是明显的掉帧现象。

更新视图

回流(Reflow)

也称为重排(Layout)。更新了元素的几何属性。需要更新完整的渲染流水线,是影响浏览器性能的关键因素。

  • 以下会触发回流:
    • 盒子模型相关属性会触发重布局(width,height,padding,margin,display,border-width,border,min-height)
    • 定位属性及浮动也会触发重布局(top,bottom,left,right,position,float,clear)
    • 改变节点内部文字结构也会触发重布局(text-align,overflow-y,font-weight,overflow,font-family,line-height,vertival-align,white-space,font-size)
    • 获取某些属性也会引发回流:
      • offset、scroll、client(Top/Left/Width/Height)
      • width,height
      • 调用了getComputedStyle(),getBoundingClientRect()或者 IE 的currentStyle

现代浏览器大多会对做优化都是通过队列机制来批量更新布局,浏览器会把修改操作放在队列中,至少一个浏览器刷新(即 16.6ms)才会清空队列。当你获取布局信息的操作的时候,会强制队列刷新,因为队列中可能有会影响这些属性或方法返回值的操作,触发回流与重绘来确保返回正确的值。

重绘(Repaint)

更新了元素的绘制属性:color,border-style,border-radius,visibility,text-decoration,background,background-image,background-position,background-repeat,background-size,outline-color,outline,outline-style,outline-width,box-shadow,渲染流水线直接从 Paint 阶段执行。

直接合成

如上面复合层已提到,使用 transform 实现动画效果,渲染流水线直接从 tiles/raster 开始,不占用渲染线程资源,大大提升绘制效率。这也是 CSS 动画比 JavaScript 动画高效的原因。(本质是利用 GPU 加速优先使用合成层合并属性)

减少重绘与回流

  • 用使用直接合成属性或重绘属性代替回流属性,如使用 transform->top,leftvisibility->display:none
  • 将动画或频繁重绘回流的节点设置为单独图层,使用 transform、opacity、filters、will-change 触发 GPU 硬件加速直接合成
  • 避免频繁操作 DOM 和样式,如将 dom 离线:documentFragmentdisplay:noneclone dom 等,设置 class 类,一次更新
  • 避免使用 table 布局,尽可能在 DOM 树的最末端改变 class
  • 避免频繁读取会引发回流/重绘的属性
  • 避免设置多层内联样式,css 表达式

外链资源的下载

在解析 html 中会遇到一些资源连接,会单独开启一个下载线程去下载资源。

遇到 CSS 样式资源

  • css 加载不会阻塞 DOM 树解析(异步加载时 DOM 照常构建)
  • 但会阻塞布局树渲染(渲染时需等 css 加载完毕,因为布局树需要 css 信息)
  • 会阻塞后面 js 脚本的执行
  • 例外:media query声明的CSS是不会阻塞渲染的

遇到 JS 脚本资源

  • 阻塞浏览器的解析,当发现外链脚本时,需等待脚本下载完成并执行后才会继续解析 HTML,之前解析的 dom 不受影响,即 FP 可能会提前
  • 如果是内联脚本,得等到页面所有脚本执行完后,才开始解析页面 dom
  • 可以加上 defer(延迟执行)async(异步执行)属性或 document.createElement('script') 来避免阻塞

遇到 img 图片类资源

  • 遇到图片等资源时,直接就是异步下载,不会阻塞解析,下载完毕后直接用图片替换原有 src 的地方。
  • 当页面上所有的 DOM,样式表,脚本,图片都已经加载完成(包括异步),触发 load 事件。

JS 引擎解析过程

前面有提到遇到 JS 脚本时,会等到它的执行。

v8 引擎工作原理

V8 是用C++编写的跨平台的 Google 开源高性能 JavaScript 和 WebAssembly 引擎,它用于 Chrome 和 Node.js 等,可以独立运行,也可以嵌入到任何C++应用程序中。

v8流程图

  1. js源码通过词法分析(分词 tokenize)将代码分解成词元(token),再通过语法分析(解析 parse)根据语法规则转为AST,生成抽象语法树(AST)和执行上下文。
  2. 通过解释器 Ignition根据AST生成字节码(ByteCode)(不直接转成机器码是因为其内存消耗大)
  3. 通过解释器 Ignition逐条解释执行执行字节码。如果有热点函数(函数被多次调用)就会经过编译器 TurboFan转换成优化的机器码,提高代码的执行性能。如果类型发生了变化,之前优化的机器码并不能正确的处理运算,就会逆向的转换成字节码。

最终计算机执行的就是CPU机器码

JIT

即时编译(JIT-Just In Time compiler)。解释器在解释执行字节码的同时将热点函数的字节码转换为机器码,并把转换后的机器码缓存起来。这样整个程序的运行速度能得到显著提升。

JS 的执行环境

即执行上下文(execution context)。js 解释器运行阶段还会维护一个环境栈(即执行栈/调用栈/栈空间 Call Stack),首次载入脚本,它将创建全局执行上下文,并压入环境栈栈顶(不可被弹出)。当执行流进入一个函数时,就会创建一个执行上下文,并把它压入环境栈的顶部,当函数执行完后会将其从环境栈弹出,并将控制权返回前一个执行环境。环境栈的顶端始终是当前正在执行的环境。

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

在 js 中,执行环境可以抽象的理解为一个 object,它由以下几个属性构成:

  • 作用域链(Scope chain)
  • 变量对象(Variable object-VO)
  • this(上下文对象)

作用域链

作用域链,它在解释器进入到一个执行环境时初始化完成并将其分配给当前执行环境。每个执行环境的作用域链由当前环境的变量对象父级环境的作用域链构成。

流程简述:在函数上下文中,查找一个变量foo,如果函数的 VO 中找到了,就直接使用,否则去它的父级作用域链中(parent)找,如果父级中没找到,继续往上找,直到全局上下文中也没找到就报错。

作用域

作用域在于指定变量、函数的作用范围,即它们可以在什么范围内被访问到,也就是它们的可访问性。实际运行的时候,JS 内核通过一系列的 VO/AO 及其连接关系来解决变量、函数的作用域管理问题。

JavaScript 采用词法作用域(lexical scoping),也就是静态作用域,作用域是由代码函数声明的位置来决定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var value = 1;
function foo() {
console.log(value);
}
function bar() {
var value = 2;
foo();
}

bar();

// 1

var scope = 'global scope';
function checkscope() {
var scope = 'local scope';
function f() {
return scope;
}
return f;
}
checkscope()();
//local scope

VO 与 AO

  • VO:每一个执行上下文都会分配一个变量对象(variable object)**,变量对象的属性由变量(variable)函数声明(function declaration)构成。在函数上下文情况下,参数列表(parameter list)也会被加入到VO**中作为属性。变量对象与当前作用域息息相关,不同作用域的变量对象互不相同,它保存了当前作用域的所有函数和变量。

  • AO:当函数被激活,那么一个活动对象(activation object)**就会被创建并且分配给执行上下文。活动对象由特殊对象arguments初始化而成。随后他被当做VO**用于变量初始化。

  • 在函数上下文中:VO === AO

  • 在全局上下文中:VO === this === global

总的来说,VO中会存放一些变量信息(如声明的变量,函数,arguments 参数等等)

一道 js 面试题引发的思考

闭包

在 JavaScript 中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。比如外部函数是 foo,那么这些变量的集合就称为 foo 函数的闭包。

javascript 闭包概念及用法梳理

this 指针

this 是执行上下文环境的一个属性Javascript 中神奇的 this

回收机制

V8 引擎的垃圾内存回收机制

CSS 相关

CSS 中规定每一个元素都有自己的盒子模型,然后按照规则摆放到页面上。

BFC

BFC 等 Formatting Contexts 浅析

居中

css 居中完整版

flex & grid

flex 布局总结
CSS Grid 布局

一些属性

rem em

  • em 作为 font-size 的单位时,其代表父元素的字体大小,em 作为其他属性单位时,代表自身字体大小。
  • rem 作用于根元素,相对于原始大小(16px),作用于非根元素,相对于根元素字体大小。

line-height

行高。取值包括:

  • normal 取决于元素的 font-family,约为 1.2
  • 数字 行高则为 数字 * font-size
  • 长度 行高即为长度: 如 16px
  • 百分比 行高则为 百分比 * font-size

padding/margin

内边距/外边距,取值除了普通长度(如 16px)之外,主要关注百分比:内边距/外边距即为 百分比*父元素的宽度,可用做高度自适应。

position:fixed

fixed 属性会创建新的层叠上下文。当元素祖先的 transform, perspectivefilter 属性非 none 时,容器由视口改为该祖先。

box-sizing

1
2
3
box-sizing: content-box; /*默认值,对应标准盒子模型 */
box-sizing: border-box; /* 对应IE盒子模型,将 padding 和 border 被包含在定义的 width 和 height 之内 */
box-sizing: inherit; /* 规定应从父元素继承 box-sizing 属性 */

CSS3 多列

  • column-count
  • column-gap
  • column-rule

其它