# 浏览器相关
# 渲染及优化
# 1. 地址栏输入url 发生了什么
- 首先会进行 url 解析,根据 dns 系统进行 ip 查找
- 根据 ip 就可以找到服务器,然后浏览器和服务器会进行 TCP 三次握手建立连接,如果此时是 https 的话,还会建立 TLS 连接以及协商加密算法,这里就会出现另一个需要注意的问题"https 和 http 的区别"(下文会讲到)
- 连接建立之后浏览器开始发送请求获取文件,此时这里还会出现一种情况就是缓存,建立连接后是走缓存还是直接重新获取,需要看后台设置,所以这里会有一个关注的问题"浏览器缓存机制",缓存我们等会在讲,现在我们就当没有缓存,直接去获取文件
- 首先获取 html 文件,构建 DOM 树,这个过程是边下载边解析,并不是等 html 文件全部下载完了,再去解析 html,这样比较浪费时间,而是下载一点解析一点
- 好了解析到 html 头部时候,又会出现一种问题,css,js 放到哪里了?不同的位置会造成渲染的不同,此时就会出现另一个需要关注的问题"css,js 位置应该放哪里?为什么",我们先按照正确的位置来说明(css 放头部,js 放尾部)
- 解析到了 html 头部发现有 css 文件,此时下载 css 文件,css 文件也是一边下载一边解析的,构建的是 CSSOM 树,当 DOM 树和 CSSOM 树全部构建完之后,浏览器会把 DOM 树和 CSSOM 树构建成渲染树。
- 样式计算, 上面最后一句"DOM 树和 CSSOM 树会一起构建成渲染树"说的有点笼统,其实还有更细一点的操作,但是一般回答到上面应该就可以了,我们现在接上面说一下构造渲染树的时候还做了哪些事情。第一个就是样式计算,DOM树 和 CSSOM树有了之后,浏览器开始样式计算,主要是为 DOM 树上的节点找到对应的样式
- 构建布局树,样式计算完之后就开始构建布局树。主要是为 DOM 树上的节点找到页面上对应位置以及一些"display:none"元素的隐藏。
- 构建分层树,布局树完成后浏览器还需要建立分层树,主要是为了满足滚动条,z-index,position 这些复杂的分层操作
- 将分层树图块化,利用光栅找到视图窗口下的对应的位图。主要是因为一个页面可能有几屏那么长,一下渲染出来比较浪费,所以浏览器会找到视图窗口对应的图块,将这部分的图块进行渲染
- 最终渲染进程将整个页面渲染出来,在渲染的过程中会还出现重排和重绘,这也是比较爱问的问题"重排重绘为什么会影响渲染,如何避免?"
# 2. js和css顺序的优化和影响
渲染树的构成必须要 DOM 树和 CSSOM 树的,所以尽快的构建 CSSOM 树是一个重要的优化手段,如果 css 文件放在尾部,那么整个过程就是一个串行的过程先解析了 dom,再去解析 css。所以 css 我们一般都是放在头部,这样 DOM 树和 CSSOM 树的构建是同步进行的
再来看 js,因为 js 的运行会阻止 DOM 树的渲染的,所以一旦我们的 js 放在了头部,而且也没有异步加载这些操作的话,js 一旦一直在运行,DOM 树就一直构建不出来,那么页面就会一直出现白屏界面,所以一般我们会把 js 文件放在尾部。当然放到尾部也不是就没有问题了,只是问题相对较小,放到尾部的 js 文件如果过大,运行时间长,代码加载时,就会有大量耗时的操作造成页面不可点击,这就是另一个问题,但这肯定比白屏要好,白屏是什么页面都没有,这种是页面有了只是操作不流畅。
js 脚本放在尾部还有一个原因,有时候 js 代码会有操作 dom 节点的情况,如果放在头部执行,DOM树还没有构建,拿不到 DOM 节点但是你又去使用就会出现报错情况,错误没处理好的话页面会直接崩掉
# 3. 重排重绘为什么会影响渲染,如何避免?
# 重绘
重绘指的是不影响界面布局的操作,比如更改颜色,那么根据上面的渲染讲解我们知道,重绘之后我们只需要在重复进行一下样式计算,就可以直接渲染了,对浏览器渲染的影响相对较小
# 重排
重排指的是影响界面布局的操作,比如改变宽高,隐藏节点等。对于重排就不是一个重新计算样式那么简单了,因为改变了布局,根据上面的渲染流程来看涉及到的阶段有样式计算,布局树重新生成,分层树重新生成,所以重排对浏览器的渲染影响是比较高的
# 避免方法
- js 尽量减少对样式的操作,能用 css 完成的就用 css
- 对 dom 操作尽量少,能用 createDocumentFragment 的地方尽量用
- 如果必须要用 js 操作样式,能合并尽量合并不要分多次操作
- resize 事件 最好加上防抖,能尽量少触发就少触发
- 加载图片的时候,提前写好宽高
# 4. DOM树和CSSOM
# DOM 树
浏览器在接受到服务器传递回来的字节流数据后,会经过转换,把0和1的字节流数据转换成 DOM 树结构,会经历如下图所示的过程:
最终可能回渲染成如下的 DOM 树结构:
# CSSDOM
与 DOM 树渲染过程类似,CSSOM 树渲染过程会经历如下图所示的过程:
# DOM 和 CSSOM 树合并
当 DOM 树和 CSSOM 树渲染完毕后,就会合并在一起形成一个渲染树,渲染树并不是简单的将 DOM 树和 CSSOM 树简单的合并在一起,渲染树只包含需要显示的 DOM 节点。渲染树合并完毕后,然后会根据渲染树进行布局,随后调用 GPU 进行绘制,显示在屏幕上。

# 5. CSS会阻塞dom解析吗?
# 6. requestIdleCallback是干什么用的
# 7. 关键渲染路径详述
# 8. 说说浏览器渲染流程,分层之后在什么时候合成
# 9. 首页白屏原因,如何解决
# 10. 如何定位和解决问题页面打开后cpu和内存快速增长的问题
# 事件机制
# 1. 注册事件
注册事件,我们一般使用addEventListener(name, callback, boolean)函数,该函数支持三个参数,参数说明如下:
name:代表待注册事件的名字,例如:click或者mouseovercallback:代表注册事件的回调函数boolean:一个boolean值,为true代表事件捕获时触发,为false时代表事件冒泡时触发。参数缺省时默认为false
// 一个注册事件的案例
// 点击DOM元素时。顺序打印出:捕获时触发 冒泡时触发
var box = document.getElementById('box')
box.addEventListener(
'click',
() => {
console.log('捕获时触发')
},
true
)
box.addEventListener(
'click',
() => {
console.log('冒泡时触发')
},
false
)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
在浏览器中,事件的触发顺序一般而言依据:捕获->目标阶段->冒泡三个顺序。但事件的触发顺序并不总是按以上顺序执行,当我们给同一个 DOM 元素同时注册捕获和冒泡事件时,事件的触发顺序是按你注册事件的顺序来执行的。
// 点击DOM元素时。顺序打印出:冒泡时触发 捕获时触发
var box = document.getElementById('box')
box.addEventListener(
'click',
() => {
console.log('冒泡时触发')
},
false
)
box.addEventListener(
'click',
() => {
console.log('捕获时触发')
},
true
)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
阻止事件冒泡: stopPropagation()和stopImmediaPropagation()方法都能阻止事件的向上冒泡,但这两者是有区别的:stopImmediaPropagation()还能阻止目标执行别的注册事件。
// 阻止事件冒泡
// 1. 当不阻止冒泡时,window的click会触发
// 2. 当使用stopPropagation()时,window的click不会被触发
// 3. 当使用stopImmediatePropagation()时,DOM的捕获事件不会触发,window的click不会触发
var box = document.getElementById('box')
box.addEventListener(
'click',
event => {
console.log('冒泡时触发')
// event.stopPropagation();
// event.stopImmediatePropagation();
},
false
)
box.addEventListener(
'click',
event => {
console.log('捕获时触发')
},
true
)
window.addEventListener('click', event => {
console.log('子元素点击事件向上冒泡时触发')
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 跨域
同源策略
同源策略是指,一个源的客户端脚本在没有明确授权的情况下,不能访问另一个源的客户端脚本。当一个 URL 和另一个 URL,只要协议、域名或者端口号有一个不同,则就会出现跨域。 解决跨域常用方法有:
- JSONP
- CORS
- document.domain
- postMessage
# 1. JSONP 实现跨域
原理
JSONP 实现跨域的原理是利用script标签没有跨域限制,通过src指向一个ajax的 URL,最后跟一个回调函数callback
// 一个JSONP跨域的案例
;<script src="http://www.baidu.com/getUserInfo?name=张三&callback=jsonp"></script>
function jsonp() {
console.log('JSONP实现跨域')
}
2
3
4
5
// 实现自己的JSONP
var jsonp = function(url, data, callback) {
var cbName = 'callback_' + new Date().getTime()
var queryString = url.indexOf('?') == -1 ? '?' : '&'
for (var k in data) {
queryString += k + '=' + data[k] + '&'
}
queryString += 'callback=' + cbName
var script = document.createElement('script')
script.src = url + queryString
window[cbName] = function(data) {
callback(data)
document.body.removeChild(script)
}
document.body.appendChild(script)
}
// 实测
jsonp('http://api.douban.com/v2/movie/in_theaters', { count: 1 }, function(data) {
console.log(data)
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 2. CORS 实现跨域
TIP
CORS:CORS需要浏览器和后端同时配合才能生效,后端通过设置Access-Control-Allow-Origin就可以开启哪些域名可以使用CORS跨域,在进行CORS跨域请求时,会出现简单请求或者复杂请求。
CORS 简单请求:当请求方式为get,head、post之一并且Content-Type为text/plain、multipart/form-data、application/x-www-form-urlencoded三种之一时,就是简单请求。
CORS 复杂请求: 当不符合简单请求时,就是复杂请求,对于复杂请求来说,首先会发送一个option请求,用于知道服务器是否允许跨域请求。
# 3. document.domain
TIP
document.domain 只能用于二级域名相同的情况下
// 域名a.test.com 和域名b.test.com
// 设置如下代码后,二级域名为test.com的网站都能实现跨域
document.domain = 'test.com'
2
3
# 4. postMessage 实现跨域
TIP
postMessage一般用于获取嵌套在页面中的第三方页面的数据,一个页面发送请求,另外一个页面判断来源并接受请求。
<body>
<iframe src="https://www.baidu.com" frameborder="0"></iframe>
</body>
2
3
// 父页面发送请求
window.frames[0].postMessage('getcolor', '*')
// 父页面接受请求
window.addEventListener(
'message',
function(e) {
console.log(e.data) // 打印red
},
false
)
// 子页面发送请求
window.addEventListener(
'message',
function(e) {
window.parent.postMessage('red', '*')
},
false
)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 浏览器存储
TIP
浏览器存储有如下四种方法,每种方法都有不同支持,具体特性请参考表格
- cookie
- localStorage
- sessionStorage
- indexDB

# 1. Cookie
设置 cookie
function setCookie(cname, cvalue, exdays) {
var d = new Date()
d.setTime(d.getTime() + exdays * 24 * 60 * 60 * 1000)
var expires = 'expires=' + d.toUTCString()
document.cookie = cname + '=' + cvalue + '; ' + expires
}
setCookie('name', 'why', 30)
2
3
4
5
6
7
获取 cookie
function getCookie(name) {
var arr,
reg = new RegExp('(^| )' + name + '=([^;]*)(;|$)')
if ((arr = document.cookie.match(reg))) return unescape(arr[2])
else return null
}
setCookie('name', 'why', 30)
console.log(getCookie('name')) // 打印why
2
3
4
5
6
7
8
# 2. localStorage 和 sessionStorage
设置 localStorage 和 sessionStorage
localStorage.setItem('name', 'why')
sessionStorage.setItem('age', 23)
2
获取 localStorage 和 sessionStorage
localStorage.setItem('name', 'why')
console.log(localStorage.getItem('name')) // 打印why
sessionStorage.setItem('age', 23)
console.log(sessionStorage.getItem('age')) // 打印23
2
3
4
# 3. 缓存机制
一个浏览器数据请求会经历三个步骤:发起网络请求、后端处理、浏览器响应。浏览器缓存可以让我们在第一步和第三步中优化性能。
# 4. 缓存位置
TIP
缓存在不同的位置,它的优先级是不同的,缓存按优先级可以划分为:
- Service Worker 可以让我们自由控制应该缓存哪些文件(PWA 实现的重要手段)
- Memory Cache(内存缓存)内存缓存读取效率高,但一旦我们关闭了浏览器,内存缓存也就没有了。
- Disk Cache(硬盘缓存)与内存缓存相比,硬盘缓存具有量大以及时效的两大优点。
- Push Cache 当前三者缓存都没有命中时,才会读取 Push Cache 中的缓存信息,但此种方式的缓存信息时间较短,只在会话
Session中存在,一旦会话结束也就释放了。 - 当以上缓存都没有命中时,才会发起请求。
# 5. 缓存策略
TIP
通常来说,浏览器缓存策略分为两种:强缓存和协商缓存,缓存策略可通过 HTTP Header 来实现。
- 强缓存: 强制缓存就是文件直接从缓存中获取 ,强缓存可以通过设置
Expires和Cache-Control来实现,强缓存表示在缓存期间,不需要请求,State Code为 200,Cache-Control可以组合使用多个指令,常见指令如下所示:
HTTP1.1 利用的就是 max-age:600 来强制缓存,因为是相对时间,所以不会出现 Expires 问题
协商缓存: 协商缓存表示如果缓存过期了,那么就需要重新发起请求验证资源是否有更新,可通过设置 HTTP Header 的
Last-Modified/if-Modified-Since,Etag/if-None-Match来实现,如果资源没有改变,State Code为 304由于
Last-Modified的时间粒度是秒,有的文件在 1s 内可能被改动多次。这种方式在这种特殊情况下还是会失效,所以HTTP1.1又引入了Etag字段。这个字段是根据文件内容生成一个标记符比如"W/"5f9583bd-10a8"",然后再和If-None-Match进行对比就能更准确的知道文件有没有被改动过- 浏览器第一次发送请求获取文件缓存下来,服务器响应头返回一个
if-Modified-Since,记录被改动的时间 - 浏览器第二次发送请求的时候会带上一个
Last-Modified请求头,时间就是if-Modified-Since返回的值。然后服务器拿到这个字段和自己内部设置的时间进行对比,时间相同表示没有修改,就直接返回** 304 **从缓存里面获取文件
- 浏览器第一次发送请求获取文件缓存下来,服务器响应头返回一个
# 6. service worker
Service Worker是运行在浏览器背后的独立进程,一般可以用来实现缓存功能,实现Service Worker的话,必须使用 https 传输协议,一个实现Service Worker缓存js文件可以如下写
if ('serviceWorker' in navigator) {
navigator.serviceWorker
.register('./1.js')
.then(success => {
console.log('注册成功')
})
.catch(error => {
console.log('注册失败')
})
}
2
3
4
5
6
7
8
9
10