共计 16277 个字符,预计需要花费 41 分钟才能阅读完成。
面试中经常会被问到这个问题吧,唉,我最开始被问到的时候也就能大概说一些流程。被问得多了,自己就想去找找这个问题的全面回答,于是乎搜了很多资料和网上的文章,根据那些文章写一个总结。
写得不好,或者有意见的直接喷,不用走流程。也欢迎大佬指点
首先这不是小问题,能把里面的过程说清楚真的很麻烦,然后下面我把这些知识点,按流程的形式总结的:
从浏览器接收 url 到开启网络请求线程
开启网络线程到发出一个完整的 http 请求
从服务器接收到请求到对应后台接收到请求
后台和前台的 http 交互
http 的缓存问题
浏览器接收到 http 数据包后的解析流程
CSS 的可视化格式模型
JS 引擎解析过程
跨域、web 安全、hybrid 等等
1. 从浏览器接收 url 到开启网络请求线程
多进程的浏览器
浏览器是多进程的,有一个主控进程,以及每一个 tab 页面都会新开一个进程(某些情况下多个 tab 会合并进程)。
进程可能包括主控进程,插件进程,GPU,tab 页(浏览器内核)等等。
Browser 进程:浏览器的主进程(负责协调、主控),只有一个
第三方插件进程:每种类型的插件对应一个进程,仅当使用该插件时才创建
GPU 进程:最多一个,用于 3D 绘制
浏览器渲染进程(内核):默认每个 Tab 页面一个进程,互不影响,控制页面渲染,脚本执行,事件处理等(有时候会优化,如多个空白 tab 会合并成一个进程)
多线程的浏览器内核
每一个 tab 页面可以看作是浏览器内核进程,然后这个进程是多线程的,它有几大类子线程:
GUI 渲染线程
JS 引擎线程
事件触发线程
定时器触发线程
http 异步网络请求线程
解析 URL
输入 URL 后,会进行解析(URL 的本质就是统一资源定位符)
URL 一般包括几大部分:
protocol,协议头,譬如有 http,ftp,https 等
host,主机域名或 IP 地址
port,端口号
path,目录路径
query,即查询参数
fragment,即 #后的 hash 值,一般用来定位到某个位置
网络请求都是单独的线程
每次网络请求时都需要开辟单独的线程进行,譬如如果 URL 解析到 http 协议,就会新建一个网络线程去处理资源下载。
因此浏览器会根据解析出得协议,开辟一个网络线程,前往请求资源。
2. 开启网络线程到发出一个完整的 http 请求
DNS 查询得到 IP
如果输入的是域名,需要进行 dns 解析成 IP,大致流程:
如果浏览器有缓存,直接使用浏览器缓存,否则使用本机缓存,再没有的话就是用 host 如果本地没有,就向 dns 域名服务器查询(当然,中间可能还会经过路由,也有缓存等),查询到对应的 IP 注意,域名查询时有可能是经过了 CDN 调度器的(如果有 cdn 存储功能的话)。
而且,需要知道 dns 解析是很耗时的,因此如果解析域名过多,会让首屏加载变得过慢,可以考虑 dns-prefetch 优化。这一块可以深入展开,具体请去网上搜索,这里就不占篇幅了(网上可以看到很详细的解答)。
tcp/ip 请求
http 的本质就是 tcp/ip 请求。需要了解三次握手规则建立连接以及断开连接时的四次挥手。tcp 将 http 长报文划分为短报文,通过三次握手与服务端建立连接,进行可靠传输。
三次握手:1. 客户端给服务器发确实是当前服务器 2. 服务器给客户端回应,我是你要访问的当前服务器 3. 客户端回应,我是客户端
四次挥手:1. 发起者:关闭主动传输信息的通道,只能接收信息 2. 接受者:收到通道关闭的信息 3. 接受者:也关闭主动传输信息的通道 4. 发起者:接收到数据,关闭通道,双方无法通信
tcp/ip 的并发限制
浏览器对同一域名下并发的 tcp 连接是有限制的(2-10 个不等)。而且在 http1.0 中往往一个资源下载就需要对应一个 tcp/ip 请求。所以针对这个瓶颈,又出现了很多的资源优化方案。(感兴趣的朋友请自行搜索,资料很多)
get 和 post 的区别
这个东西网上的资料也很多,这儿就大概描述一下在 tcp/ip 层面的区别,在 http 层面的区别请读者自行搜索:get 和 post 本质都是 tcp/ip。get 会产生一个 tcp 数据包,post 两个。
具体就是:
get 请求时,浏览器会把 headers 和 data 一起发送出去,服务器响应 200(返回数据),
post 请求时,浏览器先发送 headers,服务器响应 100continue,浏览器再发送 data,服务器响应 200(返回数据)。
然后有读者可能以前了解过 OSI 的七层:物理层、数据链路层、网络层、传输层、会话层、表示层、应用层
这儿就不班门弄虎了,列一下内容,需要深入理解的读者请自行搜索,计算机网络相关的资料。
1. 应用层 (dns,http) DNS 解析成 IP 并发送 http 请求 2. 传输层 (tcp,udp) 建立 tcp 连接(三次握手)3. 网络层 (IP,ARP) IP 寻址 4. 数据链路层 (PPP) 封装成帧 5. 物理层 (利用物理介质传输比特流) 物理传输(然后传输的时候通过双绞线,电磁波等各种介质)6. 表示层:主要处理两个通信系统中交换信息的表示方式,包括数据格式交换,数据加密与解密,数据压缩与终端类型转换等 7. 会话层:它具体管理不同用户和进程之间的对话,如控制登陆和注销过程
3. 从服务器接收到请求到对应后台接收到请求
后端的操作有点多,我这儿也就不秀自己知识面低下了,哈哈
负载均衡
对于大型的项目,由于并发访问量很大,所以往往一台服务器是吃不消的,所以一般会有若干台服务器组成一个集群,然后配合反向代理实现负载均衡。(据说现在 node 在微服务的项目方面越来越猛,大并发也不在话下,正在研究 node,希望后面能写一个心得)
简单的说:用户发起的请求都指向调度服务器(反向代理服务器,譬如安装了 nginx 控制负载均衡),然后调度服务器根据实际的调度算法,分配不同的请求给对应集群中的服务器执行,然后调度器等待实际服务器的 HTTP 响应,并将它反馈给用户。
后台的处理
一般后台都是部署到容器中的,所以一般为:
1. 先是容器接受到请求(如 tomcat 容器)2. 然后对应容器中的后台程序接收到请求(如 java 程序)3. 然后就是后台会有自己的统一处理,处理完后响应响应结果
概括下:1. 一般有的后端是有统一的验证的,如安全拦截,跨域验证 2. 如果这一步不符合规则,就直接返回了相应的 http 报文(如拒绝请求等)3. 然后当验证通过后,才会进入实际的后台代码,此时是程序接收到请求,然后执行(譬如查询数据库,大量计算等等)4. 等程序执行完毕后,就会返回一个 http 响应包(一般这一步也会经过多层封装)5. 然后就是将这个包从后端发送到前端,完成交互
4. 后台和前台的 http 交互
前后端交互时,http 报文作为信息的载体。
http 报文结构报文一般包括了:通用头部,请求 / 响应头部,请求 / 响应体。学过计算机网络的读者应超级熟悉。
通用头部这也是开发人员见过的最多的信息,包括如下:
Request Url: 请求的 web 服务器地址
Request Method: 请求方式(Get、POST、OPTIONS、PUT、HEAD、DELETE、CONNECT、TRACE)
Status Code: 请求的返回状态码,如 200 代表成功
Remote Address: 请求的远程服务器地址(会转为 IP)
譬如,在跨域拒绝时,可能是 method 为 options,状态码为 404/405 等(当然,实际上可能的组合有很多)。
其中,Method 的话一般分为两批次:
HTTP1.0 定义了三种请求方法:GET, POST 和 HEAD 方法。HTTP1.1 新增了五种请求方法:OPTIONS, PUT, DELETE, TRACE 和 CONNECT 方法。
相信知道 RESTFUL 的读者应该很熟悉,现在在前端后端开发使用频繁的也就是 get,post,put,delete,也是我们熟知的四大操作 ” 增删改查 ”。
状态码:这是进行请求和回应的关键信息,官方有最全的状态码信息,这儿就列几个常见的:
200——表明该请求被成功地完成,所请求的资源发送回客户端
304——自从上次请求后,请求的网页未修改过,请客户端使用本地缓存
400——客户端请求有错(譬如可以是安全模块拦截)
401——请求未经授权
403——禁止访问(譬如可以是未登录时禁止)
404——资源未找到
500——服务器内部错误
503——服务不可用
其他的请读者自行去搜索官方介绍。
对于状态码:数字 1 开头的表示:请求已经接收,继续处理数字 2 开头的表示:请求成功,已经被服务器成功处理数字 3 开头的表示:需要客户端采取进一步的操作才能完成请求数字 4 开头的表示:客户端看起来可能发生了错误,妨碍了服务器的处理数字 5 开头的:表示服务器在处理请求的过程中有错误或者异常状态发生,也有可能是服务器意识到以当前的软硬件资源无法完成对请求的处理
请求 / 响应头部
请求和响应头部也是分析时常用到的。常用的请求头部(部分):
Accept: 接收类型,表示浏览器支持的 MIME 类型(对标服务端返回的 Content-Type)
Accept-Encoding:浏览器支持的压缩类型, 如 gzip 等, 超出类型不能接收
Content-Type:客户端发送出去实体内容的类型
Cache-Control: 指定请求和响应遵循的缓存机制,如 no-cache
If-Modified-Since:对应服务端的 Last-Modified,用来匹配看文件是否变动,只能精确到 1s 之内,http1.0 中
Expires:缓存控制,在这个时间内不会请求,直接使用缓存,http1.0,而且是服务端时间
Max-age:代表资源在本地缓存多少秒,有效时间内不会请求,而是使用缓存,http1.1 中
If-None-Match:对应服务端的 ETag,用来匹配文件内容是否改变(非常精确),http1.1 中
Cookie:有 cookie 并且同域访问时会自动带上
Connection:当浏览器与服务器通信时对于长连接如何进行处理, 如 keep-alive
Host:请求的服务器 URL
Origin:最初的请求是从哪里发起的(只会精确到端口),Origin 比 Referer 更尊重隐私
Referer:该页面的来源 URL(适用于所有类型的请求,会精确到详细页面地址,csrf 拦截常用到这个字段)
User-Agent:用户客户端的一些必要信息,如 UA 头部等
常用的响应头部:
Access-Control-Allow-Headers: 服务器端允许的请求 Headers
Access-Control-Allow-Methods: 服务器端允许的请求方法
Access-Control-Allow-Origin: 服务器端允许的请求 Origin 头部(譬如为 *)
Content-Type:服务端返回的实体内容的类型
Date:数据从服务器发送的时间
Cache-Control:告诉浏览器或其他客户,什么环境可以安全的缓存文档
Last-Modified:请求资源的最后修改时间
Expires:应该在什么时候认为文档已经过期, 从而不再缓存它
Max-age:客户端的本地资源应该缓存多少秒,开启了 Cache-Control 后有效
ETag:请求变量的实体标签的当前值
Set-Cookie:设置和页面关联的 cookie,服务器通过这个头部把 cookie 传给客户端
Keep-Alive:如果客户端有 keep-alive,服务端也会有响应(如 timeout=38)
Server:服务器的一些相关信息
请求头部和响应头部是有对应关系的:例如 1. 请求头部的 Accept 要和响应头部的 Content-Type 匹配,否则会报错。2. 跨域请求时,请求头部的 Origin 要匹配响应头部的 Access-Control-Allow-Origin,否则会报跨域错误。3. 在使用缓存时,请求头部的 If-Modified-Since、If-None-Match 分别和响应头部的 Last-Modified、ETag 对应。
更多的对应关系请读者自行搜索。
请求 / 响应实体
做 http 请求时,除了头部,还有消息实体,一般来说,请求实体中会将一些需要的参数都放入进入(用于 post 请求)。譬如实体中可以放参数的序列化形式(a=1&b= 2 这种),或者直接放表单对象(FormData 对象,上传时可以夹杂参数以及文件),等等。
而一般响应实体中,就是放服务端需要传给客户端的内容。一般现在的接口请求时,实体中就是对于的信息的 json 格式。
cookie 以及优化
cookie 是浏览器的一种本地存储方式,一般用来帮助客户端和服务端通信的,常用来进行身份校验,结合服务端的 session 使用。
常用的场景如下:
用户登陆后,服务端会生成一个 session,session 中有对于用户的信息(如用户名、密码等),然后会有一个 sessionid(相当于是服务端的这个 session 对应的 key),然后服务端在登录页面中写入 cookie,值就是:jsessionid=xxx,然后浏览器本地就有这个 cookie 了,以后访问同域名下的页面时,自动带上 cookie,自动检验,在有效时间内无需二次登陆。
一般来说,cookie 是不允许存放敏感信息的(千万不要明文存储用户名、密码),因为非常不安全,如果一定要强行存储,首先,一定要在 cookie 中设置 httponly(这样就无法通过 js 操作了)。
另外,由于在同域名的资源请求时,浏览器会默认带上本地的 cookie,针对这种情况,在某些场景下是需要优化的。
例如以下场景:
客户端在域名 A 下有 cookie(这个可以是登陆时由服务端写入的)
然后在域名 A 下有一个页面,页面中有很多依赖的静态资源(都是域名 A 的,譬如有 20 个静态资源)
此时就有一个问题,页面加载,请求这些静态资源时,浏览器会默认带上 cookie
也就是说,这 20 个静态资源的 http 请求,每一个都得带上 cookie,而实际上静态资源并不需要 cookie 验证
此时就造成了较为严重的浪费,而且也降低了访问速度(因为内容更多了)
当然了,针对这种场景,是有优化方案的(多域名拆分)。具体做法就是:
将静态资源分组,分别放到不同的子域名下
而子域名请求时,是不会带上父级域名的 cookie 的,所以就避免了浪费
说到了多域名拆分,这里再提一个问题,那就是:
在移动端,如果请求的域名数过多,会降低请求速度(因为域名整套解析流程是很耗费时间的,而且移动端一般带宽都比不上 pc)此时就需要用到一种优化方案:dns-prefetch(让浏览器空闲时提前解析 dns 域名,不过也请合理使用,勿滥用)
gzip 压缩
首先,明确 gzip 是一种压缩格式,需要浏览器支持才有效(不过一般现在浏览器都支持),而且 gzip 压缩效率很好(高达 70% 左右)。然后 gzip 一般是由 apache、tomcat 等 web 服务器开启。
当然服务器除了 gzip 外,也还会有其它压缩格式(如 deflate,没有 gzip 高效,且不流行),所以一般只需要在服务器上开启了 gzip 压缩,然后之后的请求就都是基于 gzip 压缩格式的,非常方便。
长连接与短连接
首先看 tcp/ip 层面的定义:
长连接:一个 tcp/ip 连接上可以连续发送多个数据包,在 tcp 连接保持期间,如果没有数据包发送,需要双方发检测包以维持此连接,一般需要自己做在线维持(类似于心跳包)
短连接:通信双方有数据交互时,就建立一个 tcp 连接,数据发送完成后,则断开此 tcp 连接
然后在 http 层面:
http1.0 中,默认使用的是短连接,也就是说,浏览器没进行一次 http 操作,就建立一次连接,任务结束就中断连接,譬如每一个静态资源请求时都是一个单独的连接
http1.1 起,默认使用长连接,使用长连接会有这一行 Connection:keep-alive,在长连接的情况下,当一个网页打开完成后,客户端和服务端之间用于传输 http 的 tcp 连接不会关闭,如果客户端再次访问这个服务器的页面,会继续使用这一条已经建立的连接
注意:keep-alive 不会永远保持,它有一个持续时间,一般在服务器中配置(如 apache),另外长连接需要客户端和服务器都支持时才有效。
http 2.0
http2.0 不是 https,它相当于是 http 的下一代规范(譬如 https 的请求可以是 http2.0 规范的)。然后简述下 http2.0 与 http1.1 的显著不同点:
http1.1 中,每请求一个资源,都是需要开启一个 tcp/ip 连接的,所以对应的结果是,每一个资源对应一个 tcp/ip 请求,由于 tcp/ip 本身有并发数限制,所以当资源一多,速度就显著慢下来
http2.0 中,一个 tcp/ip 请求可以请求多个资源,也就是说,只要一次 tcp/ip 请求,就可以请求若干个资源,分割成更小的帧请求,速度明显提升。
所以,如果 http2.0 全面应用,很多 http1.1 中的优化方案就无需用到了(譬如打包成精灵图,静态资源多域名拆分等)。然后简述下 http2.0 的一些特性:
多路复用(即一个 tcp/ip 连接可以请求多个资源)
首部压缩(http 头部压缩,减少体积)
二进制分帧(在应用层跟传送层之间增加了一个二进制分帧层,改进传输性能,实现低延迟和高吞吐量)
服务器端推送(服务端可以对客户端的一个请求发出多个响应,可以主动通知客户端)
请求优先级(如果流被赋予了优先级,它就会基于这个优先级来处理,由服务器决定需要多少资源来处理该请求。)
https
https 就是安全版本的 http,譬如一些支付等操作基本都是基于 https 的,因为 http 请求的安全系数太低了。
简单来看,https 与 http 的区别就是:在请求前,会建立 ssl 链接,确保接下来的通信都是加密的,无法被轻易截取分析
一般来说,如果要将网站升级成 https,需要后端支持(后端需要申请证书等),然后 https 的开销也比 http 要大(因为需要额外建立安全链接以及加密等),所以一般来说 http2.0 配合 https 的体验更佳(因为 http2.0 更快了)
一般来说,主要关注的就是 SSL/TLS 的握手流程:
1. 浏览器请求建立 SSL 链接,并向服务端发送一个随机数–Client random 和客户端支持的加密方法,比如 RSA 加密,此时是明文传输。
2. 服务端从中选出一组加密算法与 Hash 算法,回复一个随机数–Server random,并将自己的身份信息以证书的形式发回给浏览器(证书里包含了网站地址,非对称加密的公钥,以及证书颁发机构等信息)
3. 浏览器收到服务端的证书后
验证证书的合法性(颁发机构是否合法,证书中包含的网址是否和正在访问的一样),如果证书信任,则浏览器会显示一个小锁头,否则会有提示
用户接收证书后(不管信不信任),浏览会生产新的随机数–Premaster secret,然后证书中的公钥以及指定的加密方法加密 Premastersecret,发送给服务器。
利用 Client random、Server random 和 Premaster secret 通过一定的算法生成 HTTP 链接数据传输的对称加密 key- session key
使用约定好的 HASH 算法计算握手消息,并使用生成的 session key 对消息进行加密,最后将之前生成的所有信息发送给服务端。
4. 服务端收到浏览器的回复
利用已知的加解密方式与自己的私钥进行解密,获取 Premastersecret
和浏览器相同规则生成 session key
使用 session key 解密浏览器发来的握手消息,并验证 Hash 是否与浏览器发来的一致
使用 session key 加密一段握手消息,发送给浏览器
5. 浏览器解密并计算握手消息的 HASH,如果与服务端发来的 HASH 一致,此时握手过程结束,
之后所有的 https 通信数据将由之前浏览器生成的 session key 并利用对称加密算法进行加密。
http 的缓存
前后端的 http 交互中,使用缓存能很大程度上的提升效率,而且基本上对性能有要求的前端项目都是必用缓存的。
强缓存与弱缓存缓存可以简单的划分成两种类型:强缓存(200fromcache)与 协商缓存(304)区别如下:
强缓存(200fromcache)时,浏览器如果判断本地缓存未过期,就直接使用,无需发起 http 请求
协商缓存(304)时,浏览器会向服务端发起 http 请求,然后服务端告诉浏览器文件未改变,让浏览器使用本地缓存
对于协商缓存,使用 Ctrl+F5 强制刷新可以使得缓存无效。但是对于强缓存,在未过期时,必须更新资源路径才能发起新的请求(更改了路径相当于是另一个资源了,这也是前端工程化中常用到的技巧)。
缓存头部简述上述提到了强缓存和协商缓存,那它们是怎么区分的呢?答案是通过不同的 http 头部控制。缓存中常用的几个头部:
If-None-Match/E-tag
If-Modified-Since/Last-Modified
Cache-Control/Max-Age
Prama/Expires
属于强缓存控制的:
(http1.1) Cache-Control/Max-Age
(http1.0) Pragma/Expires
注意:Max-Age 不是一个头部,它是 Cache-Control 头部的值。
属于协商缓存控制的:
(http1.1) If-None-Match/E-tag
(http1.0) If-Modified-Since/Last-Modified
可以看到,上述有提到 http1.1 和 http1.0,这些不同的头部是属于不同 http 时期的。
头部的区别首先明确,http 的发展是从 http1.0 到 http1.1,而在 http1.1 中,出了一些新内容,弥补了 http1.0 的不足。
http1.0 中的缓存控制:
Pragma:严格来说,它不属于专门的缓存控制头部,但是它设置 no-cache 时可以让本地强缓存失效(属于编译控制,来实现特定的指令,主要是因为兼容 http1.0,所以以前又被大量应用)
Expires:服务端配置的,属于强缓存,用来控制在规定的时间之前,浏览器不会发出请求,而是直接使用本地缓存,注意,Expires 一般对应服务器端时间,如 Expires:Fri,30Oct199814:19:41
If-Modified-Since/Last-Modified:这两个是成对出现的,属于协商缓存的内容,其中浏览器的头部是 If-Modified-Since,而服务端的是 Last-Modified,它的作用是,在发起请求时,如果 If-Modified-Since 和 Last-Modified 匹配,那么代表服务器资源并未改变,因此服务端不会返回资源实体,而是只返回头部,通知浏览器可以使用本地缓存。Last-Modified,顾名思义,指的是文件最后的修改时间,而且只能精确到 1s 以内
http1.1 中的缓存控制:
Cache-Control:缓存控制头部,有 no-cache、max-age 等多种取值
Max-Age:服务端配置的,用来控制强缓存,在规定的时间之内,浏览器无需发出请求,直接使用本地缓存,注意,Max-Age 是 Cache-Control 头部的值,不是独立的头部,譬如 Cache-Control:max-age=3600,而且它值得是绝对时间,由浏览器自己计算
If-None-Match/E-tag:这两个是成对出现的,属于协商缓存的内容,其中浏览器的头部是 If-None-Match,而服务端的是 E-tag,同样,发出请求后,如果 If-None-Match 和 E-tag 匹配,则代表内容未变,通知浏览器使用本地缓存,和 Last-Modified 不同,E-tag 更精确,它是类似于指纹一样的东西,基于 FileEtagINodeMtimeSize 生成,也就是说,只要文件变,指纹就会变,而且没有 1s 精确度的限制。
Max-Age 相比 Expires?
Expires 使用的是服务器端的时间,但是有时候会有这样一种情况 - 客户端时间和服务端不同步。那这样,可能就会出问题了,造成了浏览器本地的缓存无用或者一直无法过期,所以一般 http1.1 后不推荐使用 Expires。而 Max-Age 使用的是客户端本地时间的计算,因此不会有这个问题,因此推荐使用 Max-Age。
注意,如果同时启用了 Cache-Control 与 Expires,Cache-Control 优先级高。
E-tag 相比 Last-Modified?
Last-Modified:
表明服务端的文件最后何时改变的
它有一个缺陷就是只能精确到 1s,
然后还有一个问题就是有的服务端的文件会周期性的改变,导致缓存失效
E-tag:
是一种指纹机制,代表文件相关指纹
只有文件变才会变,也只要文件变就会变,
也没有精确时间的限制,只要文件一遍,立马 E -tag 就不一样了
如果同时带有 E-tag 和 Last-Modified,服务端会优先检查 E-tag。
6. 浏览器接收到 http 数据包后的解析流程
渲染流程大致如下:
1. 解析 HTML,构建 DOM 树
2. 解析 CSS,生成 CSS 规则树
3. 合并 DOM 树和 CSS 规则,生成 render 树
4. 布局 render 树(Layout/reflow),负责各元素尺寸、位置的计算
5. 绘制 render 树(paint),绘制页面像素信息
6. 浏览器会将各层的信息发送给 GPU,GPU 会将各层合成(composite),显示在屏幕上
找了个图:
HTML 解析,构建 DOM
整个渲染步骤中,HTML 解析是第一步。简单的理解,这一步的流程是这样的:浏览器解析 HTML,构建 DOM 树。
Bytes → characters → tokens → nodes → DOM 假设有下面这样一个代码
<html>
<head>
<meta name=”viewport” content=”width=device-width,initial-scale=1″>
<link href=”style.css” rel=”stylesheet”>
<title>Critical Path</title>
</head>
<body>
<p>Hello<span>web performance</span> students!</p>
<div><img src=”awesome-photo.jpg”></div>
</body>
</html>
浏览器的处理如下:
列举其中的一些重点过程:
Conversion 转换:浏览器将获得的 HTML 内容(Bytes)基于他的编码转换为单个字符
Tokenizing 分词:浏览器按照 HTML 规范标准将这些字符转换为不同的标记 token。每个 token 都有自己独特的含义以及规则集
Lexing 词法分析:分词的结果是得到一堆的 token,此时把他们转换为对象,这些对象分别定义他们的属性和规则
DOM 构建:因为 HTML 标记定义的就是不同标签之间的关系,这个关系就像是一个树形结构一样。例如:body 对象的父节点就是 HTML 对象,然后段略 p 对象的父节点就是 body 对象
最后的 DOM 树如下:
生成 CSS 规则 Bytes → characters → tokens → nodes → CSSOM 有如下 css 代码:
body {font-size: 16px}
p {font-weight: bold}
span {color: red}
p span {display: none}
img {float: right}
cssom 树:
当 DOM 树和 CSSOM 都有了后,就要开始构建渲染树了。
然后从渲染树开始生成我们看到的 html 页面。在这个过程中又一个小问题,重新构建和渲染页面:
重新构建,也称为 Reflow,即回流。一般意味着元素的内容、结构、位置或尺寸发生了变化,需要重新计算样式和渲染树
渲染页面,也称为 Repaint,即重绘。意味着元素发生的改变只是影响了元素的一些外观之类的时候(例如,背景色,边框颜色,文字颜色等),此时只需要应用新样式绘制这个元素就可以了
回流的成本开销要高于重绘,而且一个节点的回流往往回导致子节点以及同级节点的回流,所以优化方案中一般都包括,尽量避免回流。
什么会引起回流?1. 页面渲染初始化 2.DOM 结构改变,比如删除了某个节点 3.render 树变化,比如减少了 padding4. 窗口 resize5. 最复杂的一种:获取某些属性,引发回流
很多浏览器会对回流做优化,会等到数量足够时做一次批处理回流,但是除了 render 树的直接变化,当获取一些属性时,浏览器为了获得正确的值也会触发回流,这样使得浏览器优化无效,包括:
1.offset(Top/Left/Width/Height)
2.scroll(Top/Left/Width/Height)
3.cilent(Top/Left/Width/Height)
4.width,height
5. 调用了 getComputedStyle() 或者 IE 的 currentStyle
回流一定伴随着重绘,重绘却可以单独出现。所以一般会有一些优化方案,如:
1. 减少逐项更改样式,最好一次性更改 style,或者将样式定义为 class 并一次性更新
2. 避免循环操作 dom,创建一个 documentFragment 或 div,在它上面应用所有 DOM 操作,最后再把它添加到 window.document
3. 避免多次读取 offset 等属性。无法避免则将它们缓存到变量
4. 将复杂的元素绝对定位或固定定位,使得它脱离文档流,否则回流代价会很高
注意:改变字体大小会引发回流
var s = document.body.style;
s.padding = “2px”; // 回流 + 重绘
s.border = “1px solid red”; // 再一次 回流 + 重绘
s.color = “blue”; // 再一次重绘
s.backgroundColor = “#ccc”; // 再一次 重绘
s.fontSize = “14px”; // 再一次 回流 + 重绘
// 添加 node,再一次 回流 + 重绘
document.body.appendChild(document.createTextNode(‘abc!’));
资源外链的下载
上面介绍了 html 解析,渲染流程。但实际上,在解析 html 时,会遇到一些资源连接,此时就需要进行单独处理了。简单起见,这里将遇到的静态资源分为一下几大类(未列举所有):
CSS 样式资源
JS 脚本资源
img 图片类资源
遇到外链时的处理
当遇到上述的外链时,会单独开启一个下载线程去下载资源(http1.1 中是每一个资源的下载都要开启一个 http 请求,对应一个 tcp/ip 链接)。
遇到 CSS 样式资源
CSS 资源的处理有几个特点:
CSS 下载时异步,不会阻塞浏览器构建 DOM 树
但是会阻塞渲染,也就是在构建 render 时,会等到 css 下载解析完毕后才进行(这点与浏览器优化有关,防止 css 规则不断改变,避免了重复的构建)
有例外,media query 声明的 CSS 是不会阻塞渲染的
遇到 JS 脚本资源
JS 脚本资源的处理有几个特点:
阻塞浏览器的解析,也就是说发现一个外链脚本时,需等待脚本下载完成并执行后才会继续解析 HTML
浏览器的优化,一般现代浏览器有优化,在脚本阻塞时,也会继续下载其它资源(当然有并发上限),但是虽然脚本可以并行下载,解析过程仍然是阻塞的,也就是说必须这个脚本执行完毕后才会接下来的解析,并行下载只是一种优化而已
defer 与 async,普通的脚本是会阻塞浏览器解析的,但是可以加上 defer 或 async 属性,这样脚本就变成异步了,可以等到解析完毕后再执行
注意,defer 和 async 是有区别的:defer 是延迟执行,而 async 是异步执行。
简单的说(不展开):
async 是异步执行,异步下载完毕后就会执行,不确保执行顺序,一定在 onload 前,但不确定在 DOMContentLoaded 事件的前或后
defer 是延迟执行,在浏览器看起来的效果像是将脚本放在了 body 后面一样(虽然按规范应该是在 DOMContentLoaded 事件前,但实际上不同浏览器的优化效果不一样,也有可能在它后面)
遇到 img 图片类资源
遇到图片等资源时,直接就是异步下载,不会阻塞解析,下载完毕后直接用图片替换原有 src 的地方。
loaded 和 domcontentloaded
简单的对比:
DOMContentLoaded 事件触发时,仅当 DOM 加载完成,不包括样式表,图片 (譬如如果有 async 加载的脚本就不一定完成)
load 事件触发时,页面上所有的 DOM,样式表,脚本,图片都已经加载完成了
7. CSS 的可视化格式模型
html 元素按什么规则渲染,接下来提到的内容来揭晓
CSS 中规定每一个元素都有自己的盒子模型(相当于规定了这个元素如何显示)
然后可视化格式模型则是把这些盒子按照规则摆放到页面上,也就是如何布局
换句话说,盒子模型规定了怎么在页面里摆放盒子,盒子的相互作用等等
说到底:CSS 的可视化格式模型就是规定了浏览器在页面中如何处理文档树。关键字:
包含块(Containing Block)
控制框(Controlling Box)
BFC(Block Formatting Context)
IFC(Inline Formatting Context)
定位体系
浮动
CSS 有三种定位机制:普通流,浮动,绝对定位
包含块(Containing Block)
一个元素的 box 的定位和尺寸,会与某一矩形框有关,这个框就称之为包含块。元素会为它的子孙元素创建包含块,但是,并不是说元素的包含块就是它的父元素,元素的包含块与它的祖先元素的样式等有关系。
比如:
根元素是最顶端的元素,它没有父节点,它的包含块就是初始包含块
static 和 relative 的包含块由它最近的块级、单元格或者行内块祖先元素的内容框(content)创建
fixed 的包含块是当前可视窗口
absolute 的包含块由它最近的 position 属性为 absolute、relative 或者 fixed 的祖先元素创建
如果其祖先元素是行内元素,则包含块取决于其祖先元素的 direction 特性
如果祖先元素不是行内元素,那么包含块的区域应该是祖先元素的内边距边界
控制框(Controlling Box)
块级元素和块框以及行内元素和行框的相关概念块框:
块级元素会生成一个块框(BlockBox),块框会占据一整行,用来包含子 box 和生成的内容
块框同时也是一个块包含框(ContainingBox),里面要么只包含块框,要么只包含行内框(不能混杂),如果块框内部有块级元素也有行内元素,那么行内元素会被匿名块框包围
如果一个块框在其中包含另外一个块框,那么我们强迫它只能包含块框,因此其它文本内容生成出来的都是匿名块框(而不是匿名行内框)。
行内框:
一个行内元素生成一个行内框
行内元素能排在一行,允许左右有其它元素
display 的几个属性也可以影响不同框的生成:
block,元素生成一个块框
inline,元素产生一个或多个的行内框
inline-block,元素产生一个行内级块框,行内块框的内部会被当作块块来格式化,而此元素本身会被当作行内级框来格式化(这也是为什么会产生 BFC)
none,不生成框,不再格式化结构中,当然了,另一个 visibility:hidden 则会产生一个不可见的框
BFC(Block Formatting Context)
FC 即格式上下文,它定义框内部的元素渲染规则,比较抽象,比如:
FC 像是一个大箱子,里面装有很多元素
箱子可以隔开里面的元素和外面的元素(所以外部并不会影响 FC 内部的渲染)
内部的规则可以是:如何定位,宽高计算,margin 折叠等等
不同类型的框参与的 FC 类型不同,譬如块级框对应 BFC,行内框对应 IFC。
注意,并不是说所有的框都会产生 FC,而是符合特定条件才会产生,只有产生了对应的 FC 后才会应用对应渲染规则。
BFC 规则:
在块格式化上下文中,每一个元素左外边与包含块的左边相接触(对于从右到左的格式化,右外边接触右边),即使存在浮动也是如此(所以浮动元素正常会直接贴近它的包含块的左边,与普通元素重合),除非这个元素也创建了一个新的 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 不为 visible
这里提下,display:table,它本身不产生 BFC,但是它会产生匿名框(包含 display:table-cell 的框)。
IFC(Inline Formatting Context)
IFC 即行内框产生的格式上下文。
IFC 规则
在行内格式化上下文中,框一个接一个地水平排列,起点是包含块的顶部。水平方向上的 margin,border 和 padding 在框之间得到保留,框在垂直方向上可以以不同的方式对齐:它们的顶部或底部对齐,或根据其中文字的基线对齐。
行框
包含那些框的长方形区域,会形成一行,叫做行框。行框的宽度由它的包含块和其中的浮动元素决定,高度的确定由行高度计算规则决定。
行框的规则:
如果几个行内框在水平方向无法放入一个行框内,它们可以分配在两个或多个垂直堆叠的行框中(即行内框的分割)
行框在堆叠时没有垂直方向上的分割且永不重叠
行框的高度总是足够容纳所包含的所有框。不过,它可能高于它包含的最高的框(例如,框对齐会引起基线对齐)
行框的左边接触到其包含块的左边,右边接触到其包含块的右边
结合补充下 IFC 规则
浮动元素可能会处于包含块边缘和行框边缘之间,尽管在相同的行内格式化上下文中的行框通常拥有相同的宽度(包含块的宽度),它们可能会因浮动元素缩短了可用宽度,而在宽度上发生变化。
同一行内格式化上下文中的行框通常高度不一样(如,一行包含了一个高的图形,而其它行只包含文本),当一行中行内框宽度的总和小于包含它们的行框的宽,它们在水平方向上的对齐,取决于 text-align 特性。空的行内框应该被忽略。
即不包含文本,保留空白符,margin/padding/border 非 0 的行内元素,以及其他常规流中的内容 (比如,图片,inline blocks 和 inline tables),并且不是以换行结束的行框,必须被当作零高度行框对待。
总结:
行内元素总是会应用 IFC 渲染规则
行内元素会应用 IFC 规则渲染,譬如 text-align 可以用来居中等
块框内部,对于文本这类的匿名元素,会产生匿名行框包围,而行框内部就应用 IFC 渲染规则
行内框内部,对于那些行内元素,一样应用 IFC 渲染规则
另外,inline-block,会在元素外层产生 IFC(所以这个元素是可以通过 text-align 水平居中的),当然,它内部则按照 BFC 规则渲染
相比 BFC 规则来说,IFC 可能更加抽象(因为没有那么条理清晰的规则和触发条件),但总的来说,它就是行内元素自身如何显示以及在框内如何摆放的渲染规则,这样描述应该更容易理解。
关于 css 的一些别的规则,大家可以去搜搜:
如常规流,浮动,绝对定位等区别
如浮动元素不包含在常规流中
如相对定位,绝对定位,Fixed 定位等区别
如 z-index 的分层显示机制等
8. JS 引擎解析过程
这个部分的内容请参考这儿:JS 引擎解析过程直接略过了
9. 跨域、web 安全
跨域
为什么会跨域:
在浏览器同源策略限制下,向不同源(不同协议、不同域名或者不同端口)发送 XHR 请求,浏览器认为该请求不受信任,禁止请求,具体表现为请求后不正常响应
举个栗子:
那要怎么搞呢?网上的解决办法也很多,这儿列一些:
1.jsonp
2.cors
3.document.domain
4.POSTmessage
想深入看的可以浏览一下这个 跨域的常用解决方式
web 安全
这个东西,我们在面试的时候肯定会被问到 xss 攻击的问题,大家自行搜索把,这个问题的解决方案也超级多,官方文档也介绍很详细,这儿就不整了
参考:参考文章
有大佬也总结过了很详细的内容,我照搬了点儿内容,勿喷,只是学习一下。