按照网上习惯性的文章撰写路线,第二篇应该是 CSS,也确实,各种各样的标签,也是由CSS在背后默默地做属性支持。但是对于 HTML 的很多问题,跟浏览器的关系太大了,让我不禁想去好好看看 HTML 跟浏览器之间的那些事。另外,可能一提到前端,绝大多数人都会脱口而出“JavaScript”,不过我觉得 CSS 才是前端技术里最精彩绝伦的技术,所以后面我会对 CSS 做一个特殊处理,先让我先把浏览器相关面试题做一份梳理吧。梳理的过程中,发现浏览器跟 HTTP 息息相关,后面就放在同一篇啦,一起来享受吧。
先简单认识一下什么是浏览器......
1、浏览器的主要组成部分
- 用户界面:除了浏览器主窗口显示的请求的页面外,其他显示的各个部分都属于用户界面。
- 浏览器引擎:在用户界面和呈现引擎之间传送指令。
- 呈现引擎: 负责显示请求的内容。如果请求的内容是 HTML,它就负责解析 HTML 和 CSS 内容,并将解析后的内容显示在屏幕上。
- 网络:用于网络调用(如 HTTP 请求)。其接口与平台⽆关,并为所有平台提供底层实现。
- 用户界⾯后端:用于绘制基本的窗口小部件,比如组合框和窗口。其公开了与平台⽆关的通用接口,而在底层使用操作系统的⽤户界面方法。
- JavaScript 解释器:用于解析和执行 JavaScript 代码。
- 数据存储:这是持久层。浏览器需要在硬盘上保存各种数据(如 Cookie)。HTML5 定义了“网络数据库”,这是⼀个完整(轻便)的浏览器内数据库。Chrome 浏览器的每个标签⻚都是⼀个独立的进程。
2、浏览器内核
什么是浏览器内核
浏览器内核作为浏览器最核心的部分 “Rendering Engine”,也就是渲染引擎,它决定了浏览器如何解析网页语法并渲染网页内容。
常见的浏览器内核
浏览器 /RunTime | 内核(渲染引擎) | JavaScript 引擎 |
Chrome | Blink ( 28~ ) Webkit ( Chrome 27 ) | V8 |
FireFox | Gecko | SpiderMonkey |
Safari | Webkit | JavaScriptCore |
Edge | EdgeHTML | Chakra(for JavaScript) |
IE | Trident | Chakra(for JScript) |
PhantomJS | Webkit | JavaScriptCore |
Node.js | - | V8 |
上面其实做个简单的了解就可以了,接下来是关于浏览器我觉得最精彩的一个问题,其中又包含了多个经典的面试题。在以前的文章我有做过总结,这里我再次梳理一下。
3、从浏览器输入一个url到显示页面经历的过程
这个过程其实就是一次完整的 http(1.0) 请求过程:
- 浏览器对输入的网址进行DNS解析,得到对应的IP地址
- 根据这个IP,找到对应的服务器,发起TCP连接(TCP的三次握手)
- 建立TCP链接后,发起HTTP请求
- 服务器响应HTTP请求,浏览器得到 html 代码
- 浏览器解析 html 代码,再请求代码中的资源(js、css、图片等)
- 浏览器渲染页面
- 服务器断开TCP连接(TCP的四次挥手)
整个过程清晰可见,让我们开始逐个击破吧 ~~~
URL是啥?
URL(Uniform Resource Locator),统一资源定位符,用于定位互联网上资源,俗称网址。
看一下它的定义规则:
scheme://host.domain:port/path/filename
各部分解释如下:
- scheme:定义因特网服务的类型。常见的协议有 http、https、ftp、file,其中最常见的类型是 http,而 https 则是进行加密的网络传输。
- host:定义域主机(http 的默认主机是 www)
- domain:定义因特网域名,比如 w3school.com.cn
- port:定义主机上的端口号(http 的默认端口号是 80)
- path:定义服务器上的路径(如果省略,则文档必须位于网站的根目录中)。
- filename:定义文档/资源的名称
DNS怎么找到域名的?
我们先看几个小概念
- DNS:一个网络服务器
- DNS 协议:提供通过域名查找 IP 地址,或逆向从 IP 地址反查域名的服务
- DNS域名解析:即在 DNS 上记录一条信息记录(域名对应的 IP 地址)
那浏览器如何通过域名去查询 URL 对应的 IP 呢?
过程(面试):浏览器自身域名缓存区找 =》操作系统的域名缓存区找 =》hosts文件找 =》域名服务器找
为什么HTTP协议要基于TCP来实现?
TCP是一个端到端的可靠的面相连接的协议,HTTP基于传输层TCP协议,不用担心数据传输的各种问题(当发生错误时,会重传)
TCP的三次握手和四次挥手
三次握手:
- 第一次握手:建立连接时,客户端发送syn包(syn=x)到服务器,等待服务器确认
- 第二次握手:服务器收到syn包,必须确认客户的SYN(ack=x+1),同时自己也发送一个SYN包(syn=y),即SYN+ACK包,
- 第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=y+1),此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsICM38FdsYkRGZkRG9lcvx2bjxiNx8VZ6l2csADbyIWMoJjYxgmMMBjVtJWd0ckW65UbM5WOHJWa5kHT20ESjBjUIF2X0hXZ0xCMx81dvRWYoNHLrdEZwZ1Rh5WNXp1bwNjW1ZUba9VZwlHdssmch1mclRXY39CXldWYtlWPzNXZj9mcw1ycz9WL49zZuBnLyYTMwUDOwIjMzIjNwEjMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
面试时可这样讲述:
- 第一次握手:由浏览器发起,告诉服务器我要发送请求了
- 第二次握手:由服务器发起,告诉浏览器我准备接受了,你赶紧发送吧
- 第三次握手:由浏览器发送,告诉服务器,我马上就发了,准备接受吧
面试官可能还问你,为什么需要三次握手?---- 为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。
四次挥手:
- 第一次挥手:客户端进程发出连接释放报文(FIN报文),并且停止发送数据
- 第二次挥手:服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v
- 客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)
- 第三次挥手:服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1
- 第四次挥手:客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1。服务器只要收到了客户端发出的确认,立即进入CLOSED状态。
面试时可这样讲述:
- 第一次挥手:由浏览器发起,告诉服务器,我请求报文发送完了,你准备关闭吧
- 第二次挥手:由服务器发起,告诉浏览器,我请求报文接受完了,准备关闭了,你也准备吧
- 第三次挥手:由服务器发起,告诉浏览器,我响应报文发送完了,你准备关闭吧
- 第四次挥手:由浏览器发起,告诉服务器,我响应报文接受完了,准备关闭了,你也准备吧
浏览器是如何渲染页面的?
- 构建DOM树:渲染引擎解析HTML文档,将标签转换成DOM节点构建DOM树
- 生成渲染树:解析CSS文件(生成CSS规则树),再生成渲染树,使每个节点有自己的样式
- 布局渲染树:从根节点递归调用,布局出每个节点的位置、尺寸(布局)
- 绘制渲染树:遍历渲染树,使用UI层绘制每个节点 ,呈现界面
既然涉及到了布局和渲染,那怎么能少了重绘和回流呢 ~~~
4、对重绘和回流的了解
概念
- 重绘:元素外观(如背景颜色)改变引起的浏览器行为,使元素外观重新绘制
- 回流(重排):渲染树中的元素布局或几何属性(如尺寸、隐藏)等改变,需要重新构建
注意点
- 每个页面至少发生一次回流,即页面第一次加载的时候
- 回流必定引发重绘,重绘不一定引发回流
补充
触发回流的条件:任何页面布局或几何属性的改变
- 页面渲染初始化(无法避免)
- 添加或删除可见的DOM元素
- 元素位置的改变,或使用动画
- 元素尺寸的改变(大小,外边距,边框)
- 浏览器窗口尺寸的变化(resize事件)
- 填充内容的改变(文本或图片大小改变,引起计算值宽高改变)
- 读取某些元素属性(offsetLeft/Top/Height/Width, clientLeft/Top/Height/Width,scrollLeft/Top/Height/Width等)
如何优化?
重绘回流会造成耗时、浏览器卡顿,那么如何做优化呢?(尽量减少DOM操作)
- 浏览器优化:浏览器会把引起回流、重绘的操作放入一个队列,等队列中的操作到了一定数量或者时间间隔,就 flush 这个队列进行一个批处理。这样就可以让多次回流重绘变成一次。
- 代码优化:减少对渲染树的操作,可以合并多次 DOM 和样式的修改,并减少对样式的请求。
一些代码优化操作举例:
- 修改元素样式的时候,直接修改样式名className(尽量一次修改元素样式,不要一会改一点。也就是把新样式放在另一个样式名中)
- 某些元素先设置成display: none,然后进行页面布局操作,再设置display: block(这样只会引发两次重绘回流)
- 使用cloneNode和repalceChild技术(引发一次重绘回流)
- 将需要多次回流的元素,position属性设为absoluted或fixed(元素脱离文档流,变化不会影响其他元素)
- 当需要创建多个节点的时候,使用DocumentFragment创建完后一次性的加入(如循环创建一个li,让循环结束后所有的li都创建完了(fragment中)再一次性加入(文档))
顺利完成了一次资源的请求,我们浏览器拿到了服务器发送过来的资源,为了节省性能,当然少不了存储了,那么请列出你所知道的浏览器的一些存储办法以及它们的区别吧 ~
5、cookie,session,storage的区别
这一块以前做过一张还不错的表,这里附上,并做了一些改进。
本地存储区别表
WebStorage(HTML5) | ||||
cookie | session(服务器) | sessionstorage | localstorage | |
数据生命周期 | 一般由服务器生成,在设置失效时间(expires)内有效(与窗口或浏览器是否关闭无关)。若在浏览器设置,默认浏览器关闭后失效 | 除非被清除,否则永久保存(刷新页面数据依旧存在) | 仅在当前会话有效,关闭窗口或浏览器后清除 | 除非web应用主动删除,否则永不失效 |
存放数据 | 4kb左右,数量最多20条,存储字符串 | 5M,存储对象 | 5M,只能存储字符串 | |
与服务端通信 | 保存在浏览器端。每次都会携带在HTTP请求头中,参与服务器通信 | 保存在服务器端,不参与服务器通信。会占用服务器性能 | 保存在客户端(本地存储),不参与服务器通信 | |
安全性 | 安全性较低(cookie诈骗cookie截取) | session安全性大于cookie | ||
易用性 | 一般接口需要自己封装 | 接口可以直接使用 | ||
作用域 | 在浏览器所有的同源窗口中共享 | 不能在不同的浏览器窗口共享 | 不能在不同的浏览器窗口共享,在同源窗口中可以共享 | |
使用场景 | 主要用于保存登录信息 1、判断用户是否登录过网站,方便下次登录实现自动登录或记住密码 2、上次登录的时间等信息 3、上次查看的页面 4、浏览计数 | 用于保存每个用户的专用信息,变量的值保存在服务器端,通过sessionID来区分不同用户。 1、购物车 2、用户登录信息 3、将某些数据放入session中,供同一用户的不同页面使用 4、防止用户非法登录 | 敏感账号一次性登录、表单,对于那种只需要在用户浏览一组页面期间保存而关闭浏览器后就可以丢弃的数据,sessionStorage会非常方便 | 常用于长期登录(判断用户是否登录),适合长期保存在本地的数据。 购物车信息、HTML5游戏产生的一些本地数据 |
优点 | 具有极高的扩展性和可用性 | 1、存储空间大 2、节省网络流量 3、可在本地直接获取,不需要与服务器交互 4、获取速度快 5、安全性较高 6、更多丰富易用的API接口 7、支持事件通知机制,可以将数据更新的通知发送给监听者 8、操作方便:setItem、getItem、removeItem、clear、key、length 9、临时存储 | ||
缺点 | 1、大小受限 2、用户可以禁用cookie,使功能受限 3、安全性较低 4、有些状态不能保存在客户端 5、同源请求时会被携带(服务端和客户端互传,不论是否需要),加大http流量,数据过多影响性能 6、cookie数据有路径(path)的概念,可以限制cookie只属于某个路径下 |
对浏览器的存储方式有了清晰的了解以后,我们应该再思考,存下的数据如何使用会性能更好?存下的数据我们是否需要做适时的更新?浏览器底层有什么特别的机制?
6、浏览器缓存策略
面试的时候,少不了关于性能优化的问题,基于上一个问题,不难想到浏览器端应该做的一个优化手段 ==> 减少 HTTP 请求。为此我们可以做 HTTP 缓存控制,也就是浏览器缓存策略。
关于浏览器缓存的初步回答
- 浏览器(HTTP)缓存能够帮助服务器提高并发性能,很多资源不需要重复请求,可直接从浏览器中拿缓存(通过 HTTP 获取的资源)
- 浏览器缓存分类:强缓存、协商缓存
- 强缓存通过 Expires 和 Cache-control 控制,协商缓存通过 Last-modify 和 Etag 控制
这是一个比较笼统的回答,不过我觉得十分的精辟且可聊性较高。因为它引出了三个重要的问题:浏览器的缓存策略、缓存分类及缓存访问。
浏览器缓存策略(机制)
缓存策略主要发生在三个对象之间:浏览器、浏览器缓存、服务器
- 浏览器每次发起请求,都会先在浏览器缓存中查找该请求的结果以及缓存标识
- 浏览器每次拿到返回的请求结果都会将该结果和缓存标识存入浏览器缓存中
我们根据请求结果和缓存标识来判断是否需要向服务器重新发起 HTTP 请求,而这个过程我们使用的缓存策略也不相同。
缓存策略都是通过设置 HTTP Header 来实现的,基于此我们把浏览器缓存分为强缓存和协商缓存。这两个缓存其实就是浏览器做缓存时的不同处理过程。
强缓存
不会向服务器发送请求,直接从浏览器缓存中读取资源
HTTP Header 实现: 和 Cache-Control
- Expires(http1.0):缓存过期时间(绝对时间),用来指定资源到期的时间,是服务器端的具体的时间点(在响应http请求时告诉浏览器在过期时间前浏览器可以直接从浏览器缓存取数据,而无需再次请求)。受限于本地时间,如果修改了本地时间,可能会造成缓存失效。
- Cache-Control(http1.1):是一个相对时间,代表资源的有效期。(优先)
现在基本上都会同时设置 Expires 和 Cache-Control
强缓存判断是否缓存的依据来自于是否超出某个时间或者某个时间段,而不关心服务器端文件是否已经更新,这可能会导致加载文件不是服务器端最新的内容,那我们如何获知服务器端内容是否已经发生了更新呢?此时我们需要用到协商缓存。
协商缓存
强缓存未命中,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程(判断该标识对应的资源是否更新)。
看看协商缓存的结果情况:
- 协商缓存生效,返回 304 和 Not Modified,告诉浏览器使用本地缓存
- 协商缓存失效,返回 200 和 请求结果(新资源),并存入缓存
强缓存未命中,是什么时候才未命中呢?协商缓存,协商的是什么呢?
其实就是:浏览器问服务器,我缓存的资源有没有更新啊?
- 没有更新:浏览器可以用缓存(304)
- 更新了:浏览器不能用缓存,服务器发新的给浏览器(200)
HTTP Header 实现:Last-Modified 和 ETag (帮助浏览器跟服务器进行协商)
- Last-Modified:浏览器第一次请求一个资源的时候,服务器返回的header中会加上Last-Modify,Last-modify是一个时间标识该资源的最后修改时间 。当浏览器再次请求该资源时,发送的请求头中会包含If-Modify-Since,该值为缓存之前返回的Last-Modify。服务器收到If-Modify-Since后,根据资源的最后修改时间判断是否命中缓存。如果命中缓存,则返回http304,并且不会返回资源内容,并且不会返回Last-Modify。由于对比的服务端时间,所以客户端与服务端时间差距不会导致问题。但是有时候通过最后修改时间来判断资源是否修改还是不太准确(Last-Modified 只能以秒计时)。于是出现了ETag/If-None-Match,根据资源内容是否修改来决定缓存策略。
- Etag(http1.1):服务器响应请求时,返回当前资源文件的一个唯一标识(由服务器生成),Etag/If-None-Match返回的是一个校验码 。ETag可以保证每一个资源是唯一的,只要资源有变化,Etag就会重新生成。服务器根据浏览器上发送的If-None-Match值来判断是否命中缓存。
HTTP1.1中 Etag 的出现主要是为了解决几个 Last-Modified 比较难解决的问题:
- Last-Modified标注的最后修改只能精确到秒,如果某些文件在1秒钟以内,被修改多次的话,它将不能准确标注文件的修改时间
- 如果某些文件会被定期生成或者改完又改回来,内容并没有任何变化,但Last-Modified却改变了,导致文件没法使用缓存
- 有可能存在服务器没有准确获取文件修改时间,或者与代理服务器时间不一致等情形
可能面试官会问你,Last-Modified 与 ETag 哪个更好呢?
- 精度上:Last-Modified 单位是秒,ETag 单位是每次, ETag精确度更优
- 性能上:Last-Modified 只需要记录时间,而 Etag 需要服务器通过算法来计算出一个hash值。故 Last-Modified 性能更好
- 优先级:服务器校验优先考虑 Etag
关于优先级的补充:Etag 是服务器自动生成或者由开发者生成的对应资源在服务器端的唯一标识符,能够更加准确的控制缓存。Last-Modified 与 ETag 是可以一起使用的,服务器会优先验证 ETag,一致的情况下,才会继续比对 Last-Modified,最后才决定是否返回304。
缓存访问
这个过程我们针对强缓存和协商缓存进行讨论(借用大佬的图片)
强缓存优先于协商缓存进行,若强缓存(Expires和Cache-Control)生效则直接使用缓存,若不生效则进行协商缓存(Last-Modified / If-Modified-Since和Etag / If-None-Match),协商缓存由服务器决定是否使用缓存,若协商缓存失效,那么代表该请求的缓存失效,返回200,重新返回资源和缓存标识,再存入浏览器缓存中;生效则返回304,继续使用缓存。
唔 ~ 终于理完了,太不容易了。面试的时候能说到这里已经很牛了哇,但是调皮的面试官可能还会问一个比较细的问题。 emmmm......
如果什么缓存策略都没设置,那么浏览器会怎么处理?
对于这种情况,浏览器会采用一个启发式的算法,通常会取响应头中的 Date 减去 Last-Modified 值的 10% 作为缓存时间。
其他
实际应用场景
- 频繁变动的资源
- 不常变化的资源
用户行为对浏览器缓存的影响
新开窗口 | 有效 | 有效 |
---|---|---|
用户操作 | Expires/Cache-Control | Last-Modified/Etag |
地址栏回车 | 有效 | 有效 |
页面链接跳转 | 有效 | 有效 |
前进、后退 | 有效 | 有效 |
F5 刷新 | 无效 | 有效 |
Ctrl+F5 刷新 | 无效 | 无效 |
说到浏览器的策略,除了缓存策略,我们不得不提的就是浏览器同源策略了 ~~
7、浏览器同源策略
同源
源:
- 协议
- 域名
- 端口
同源即协议、域名和端口都相同。
什么是同源策略
浏览器的同源策略是一种安全功能,同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的安全机制(不同源之间,不能进行交互)
如 google.com下的 js脚本采用 ajax 读取 baidu.com里面的文件数据是会报错的。
既然说到 ajax ,脑海里不禁涌现出一个又爱又恨的面试题了哈哈哈哈,敲了这么多文字,动手来点代码疏通下筋骨吧。
来,给我手写一个原生 ajax ~~~ (嘘,简单的就行啦!)
var xhr = new XMLHttpRequest(); // 创建 Ajax 对象
xhr.open('get', 'https://blog.csdn.net/huohuoit'); // 告诉 Ajax 请求地址以及请求方式
xhr.send(); // 发送请求数据
xhr.onreadystatechange = function () { // 获取服务器端给与客户端的响应数据
if (xhr.readyState == 4 && xhr.status == 200) {
console.log(xhr.responseText);
}
}
// 啊 ~~~ 报错啦!~~~
限制问题
浏览器中的大部分内容都是受同源策略限制的,如:
- Cookie、LocalStorage、IndexedDB 等存储性内容
- DOM 节点
- AJAX 请求发送后,结果被浏览器拦截了
- 一些第三方插件如 FLASH
但是有一些资源时不受同源策略限制的:
- 页面中的链接,重定向以及表单提交
- <script>、<img>、<iframe>、<link>、<video> 等标签
在浏览器中,<script>、<img> 、<iframe>、<link>、<video> 等标签都可以跨域加载资源,而不受同源策略的限制,通过 src 属性加载的资源,浏览器都会发起一个 GET 请求,但是浏览器限制了 JavaScript 的权限,使用 js 不能读、写加载的内容。
你可以通过这几个标签来跨域加载资源,但是,发起的GET请求返回的数据,通过 js 获取不到。
注意:通过 <script> 标签获取 js 文件里的全局属性,方法等,可以通过 js 读取到。是因为这些都是挂载在 window对象上的。
上面我们提到了跨域,相信你经常会在面试题上看到这个问题,那就顺着上文开始跟我一起拿下它吧。
8、跨域
什么是跨域
什么是跨域呢?我们上面提到了同源,同源中有“三个同”,其中一个就是“域”,但是这里的域不是这个域哈,这里的域是指“源”,也即域名地址。
上面我们讨论到,浏览器同源策略下,会引起不同源之间不能进行交互的问题。那么跨域其实就是解决不同源之间请求发送数据、通信等交互问题的解决方法。
跨域的实现
jsonp(最经典的跨域方案)
哦豁?json?jsonp?两者其实没啥关系,只是 jsonp 请求后得到的是 json 数据格式
我们可以更详细一点:
- jsonp 是 JSON With Padding(填充式 json 或参数式 json )的简写
- 组成(两部分):回调函数 和 数据。回调函数是用来处理服务器端返回的数据,回调函数的名字一般是在请求中指定的。而数据就是我们需要获取的数据,也就是服务器端的数据。
jsonp 实现跨域的请求原理:
动态创建<script>标签,然后利用<script>的 src 属性不受同源策略约束来跨域获取数据。
你可能会跟我一样疑惑,为什么是 <script> 标签?
在上一个问题的最后,我们说到 <script>、<img>、<iframe>、<link>、<video>等标签可以跨域加载资源,但是为啥只有 <script>标签可以请求到数据呢。
<script> 在请求得到数据后,遇到 js 代码,就会解析执行( js 文件里写的代码肯定要被执行的)
jsonp 的 优点:
- 实现简单
- 兼容性⾮常好
jsonp 的缺点:
- 只⽀持 get 请求(因为 <script> 标签只能get)
- 有安全性问题,容易遭受xss攻击
- 需要服务端配合 jsonp 进⾏⼀定程度的改造
BulingBuling ~~~