前端知识整理-浏览器篇
浏览器的组成
User Interface(用户交互)
浏览器的交互界面,比如前进、返回按钮,导航栏等。
Browser Engine(浏览器引擎)
是用户交互和渲染引擎的桥梁,接收用户交互通过渲染引擎将信息展示在用户界面。
Rendering Engine(渲染引擎)
浏览器的渲染引擎,负责解析html、css、js等代码,将代码渲染到用户界面。
- Chrome、Opera: Blink引擎
- Firefox:Gecko引擎
- Safari: WebKit引擎
- Edge:Trident
Networking(网络请求)
浏览器通过http、https、webbsocket、ftp等协议发送请求,接收响应。
JS Interpreter(JS解释器)
- Chrome: V8引擎
- Mozilla:SpiderMonkey引擎
- Safari:JavaScriptCore / Nitro WebKit
- Edge:Chakra
- Opera: Carakan
UI Backend(用户界面后台)
用于绘制基本的窗口小部件,比如下拉列表、文本框、按钮等,向上提供公开的接口,向下调用操作系统的用户界面。
Data Persistent(数据可持续化)
- Cookie
Cookie用于存储少量数据,一般 < 4K。 - Web Storage
LocalStorage、SessionStorage存储的数据一般是5M到10M。 - Web SQL
Web SQL 已经过时,不再成为W3C的标准。 - IndexedDB
W3C标准中推荐的数据持久化方案,浏览器允许起容量为>250M大小。通过建值对存储、支持事物、异步执行、同源策略且支持二进制存储。 - File System
主要是支持浏览器访问本地文件资料。注意File API和File System Access API是两套规范,File API只能读不能写,File System Access API支持读写,但是目前浏览器支持的不是很好。
浏览器的事件循环
浏览器的进程模型
进程和线程
进程是系统进行资源调度的基本单位,一个进程可包含多个线程,线程是系统进行运算调度的最小单位。浏览器的三大进程
浏览器进程
浏览器事件、管理tab等浏览器操作网络进程
实现浏览中的网络请求渲染进程
每个tab新开一个进程,处理html、css、js等代码执行
- 渲染主线程
渲染主线程如何实现异步
1
2
3
4
5js是一⻔单线程的语言,这是因为它运行在浏览器的渲染主线程中,而渲染主线程只有一个。
渲染主线程承担着诸多的工作,渲染⻚面、执行js都在其中运行。如果使用同步的方式,就极有可能导致主线程产生阻塞,从而导致消息队列中的很多其他任务无法得到执行。
这样一来,一方面会导致繁忙的主线程白白的消耗时间,另一方面导致⻚面无法及时更新,给用户造成卡死现象。
所以浏览器采用异步的方式来避免。具体做法是当某些任务发生时,比如计时器、网络、事件监听,主线程将任务交给其他线程去处理,自身立即结束任务的执行,转而执行后续代码。
当其他线程完成时将事先传递的回调函数包装成任务,加入到消息队列的末尾排队等待主线程调度执行。在这种异步模式下浏览器永不阻塞,从而最大限度的保证了单线程的流畅运行。JS的事件循环
1
2
3事件循环又叫做消息循环,是浏览器渲染主线程的工作方式。
在Chrome的源码中,它开启一个不会结束的for循环,每次循环从消息队列中取出第一个任务执行,而其他线程只需要在合适的时候将任务加入到队列末尾即可。过去把消息队列简单分为宏队列和微队列,这种说法目前已无法满足复杂的 浏览器环境,取而代之的是一种更加灵活多变的处理方式。
根据W3C官方的解释,每个任务有不同的类型,同类型的任务必须在同一个队列,不同的任务可以属于不同的队列。不同任务队列有不同的优先级, 在一次事件循环中,由浏览器自行决定取哪一个队列的任务。但浏览器必须有一个微队列,微队列的任务一定具有最高的优先级,必须优先调度执行。消息队列优先级
1)延时队列:用于存放计时器到达后的回调任务,优先级「中」
2)交互队列:用于存放用户操作后产生的事件处理任务,优先级「高」
3)微队列:用户存放需要最快执行的任务,优先级「最高」1
Promise.resolve().then(function) 可将function函数放入微队列
浏览器的渲染原理
浏览器通过网络进程获取html交给渲染进程渲染。
渲染进程生成渲染任务交给消息队列,渲染主线程根据任务队列中的任务进行渲染。
解析html (Parse html)
- 生成dom树,js中通过document对象操作dom树。
- 生成cssom树,js中通过documn.styleSheets获取cssom树对其操作。
- 解析遇到css代码,浏览器会启动一个预解析器去先下载和解析css。解析遇到js代码,渲染主线程会暂停等网络线程下载完js后再解析js。
样式计算 (Recalculate style)
主线程依次为树中的每个节点计算出它最终的样式,在这一过程中很多预设值会变成绝对值,比如red
会变成rgb(255,0,0)
;相对单位会变成绝对单位,比如em
会变成px
。这一步完成后,会得到一棵带有样式的DOM树。布局 (Layout)
根据css样式计算dom节点的几何信息,例如节点尺寸、相对包含块的位置。完成后会得到布局树,因为受css样式影响,dom树和布局树并不一一对应。分层 (Layer)
为了提高重绘效率,浏览器会将页面划分若干层,每次重绘只会重新渲染对应分层的内容。可以通过will-change: [transform]属性来影响分层。生成绘制指令 (Paint)
主线程会为每个层单独产生绘制指令集,用于描述这一层的内容该如何画出来。之后主线程将绘制指令集交给合成线程,之后的工作将由合成线程完成。分块 (Tiling)
为了提高效率,合成线程将每个图层进行更小区域的分块,合成线程通过线程池完成此工作。光栅化 (Raster)
分块完成后合成线程将分块信息交给GPU进程,GPU进程会先将靠近视口的分块光栅化(即计算出每块的位图[像素信息])。此步骤会涉及GPU硬件加速。画 (Draw)
合成线程拿到每个层、每个块的位图后,生成一个个「指引(quad)」信息。指引会标识出每个位图应该画到屏幕的哪个位置,以及会考虑到旋转、缩放等变形。变形发生在合成线程,与渲染主线程无关,这就是transform
效率高的本质原因。最终合成线程会把 quad 提交给 GPU 进程,由 GPU 进程产生系统调用,提交给 GPU 硬件,完成最终的屏幕成像。
资源提示关键词
为了提高页面资源加载的效率,浏览器提供了资源提示关键词。资源提示关键词可以通过 caniuse 网站查询浏览器的兼容性。
针对script资源的加载
async
script资源的网络请求和渲染主线程异步执行,资源下载完之后渲染主线程等待script执行完再继续。1
<script async src="xxx.js"></script>
defer
script资源的网络请求和渲染主线程异步执行,资源下载完之后需等待渲染主线程执行完再执行script。1
<script defer src="xxx.js"></script>
针对script、css和img等其他资源
preload
通过preload向浏览器申明一个需要提前加载的资源,当资源真正被使用时,无需再发用网络请求立即执行。1
2
3
4<link rel="preload" href="xxx.js" as="script">
<link rel="preload" href="xxx.css" as="style">
<link rel="preload" href="xxx.png" as="image">
<link rel="preload" href="xxx" as="font">prefetch / dns-prefetch
利用浏览器空闲时间加载非首页的后续页面的资源。1
2
3<link rel="prefetch" href="xxx.js" as="script">
// 通过dns-prefetch提前解析域名,资源请求速度
<link rel="dns-prefetch" href="xxx.com">prerender
prerender 和 prefetch 类似,同样会收集用户接下来可能会用到的资源,不同的是prerender会在后台提前渲染出页面。1
<link rel="prerender" href="xxx.html">
preconnect
通过preconnect提前连接资源,提高资源加载速度。1
<link rel="preconnect" href="xxx.js" as="script">
浏览器缓存
Service Worker
Memery Cache
Disk Cache
Push Cache
跨标签页通信
BroadCast(需要同源)
1
2
3
4
5
6
7
8
9// tab1
const bc = new BroadcastChannel('testChannel')
bc.postMessage({value: 'Hello World'})
// tab2
const bc = new BroadcastChannel('testChannel')
bc.onmessage = function(msg){
console.log(msg.data) // {value: 'Hello World'}
}LocalStorage(需要同源)
利用LocalStorage存储信息时所触发的onstorage事件实现跨tab通信1
2
3
4
5
6
7// tab1
localStorage.age = 20
// tab2
localStorage.onstorage = function(e) {
console.log(e) // {key: 'age', oldValue: undefinded, newValue: 20, url: 'xxxx', storageArea: xxx}
}ServiceWorker(需要同源)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23// tab1
const sw = navigator.serviceWorker.register('sw.js').then(()=>{
console.log('register success')
})
btn.onclick = function() {
navigator.serviceWorker.controller.postMessage({value: 'Hello World'})
}
// sw.js
self.addEventListener('message', async e => {
const clients = await self.clients.matchAll()
clients.forEach(client => {
client.postMessage(e.data)
})
})
// tab2
const sw = navigator.serviceWorker.register('sw.js').then(()=>{
console.log('register success')
})
navigator.serviceWorker.onmessage = function(e) {
console.log(e.data)
}SharedWorker(需要同源)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37// tab1
<input id="input"/>
<button id="btn">test1</button>
<script>
const input = document.getElementById('input');
const btn = document.getElementById('btn');
const worker = new SharedWorker('worker.js');
btn.onclick = function() {
worker.port.postMessage(input.value);
}
</script>
// worker.js
var data = '';
onconnect = function(e) {
const port = e.ports[0];
port.onmessage = function(_e) {
if(_e.data === 'get') {port.postMessage(data);} else {data = _e.data;}
};
}
// tab2
<button id="btn">test2</button>
<span id="content">0</span>
<script>
const btn = document.getElementById('btn');
const content = document.getElementById('content');
const worker = new SharedWorker('worker.js');
worker.port.start();
worker.port.onmessage = function(e) {
console.log(e);
content.innerHTML = e.data
}
btn.onclick = function() {
worker.port.postMessage('get');
}
</script>cookie (需要同源)
1
2
3
4
5
6
7
8
9// tab1
document.cookie = 'name=Tom';
// tab2
const cookie = document.cookie;
setInterval(() => {
if(document.cookie !== cookie){
console.log('cookie changed');
}
},1000)window.postMessage (可跨源)
两个tab必须是具有相同的通信协议(比如https),必须是处于同意服务器(document.domain相同)。1
2
3
4
5
6
7
8
9// index1.html
const win = window.open('index2.html', '_blank', 'width=1000,height=1000,resizeable=yes')
btn.onclick = function() {
win.postMessage('test');
}
// index2.html
window.addEventListener('message', function(e) {
console.log(e.data);
})WebSocket
WebSocket区别后台HTTP服务器最大的特点就是它是双向通信的(服务器可以向客户端主动推送消息)。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26// server.js
// 先安装依赖: npm i ws
const WebSocketServer = require('ws').Server;
const wss = new WebSocketServer({post: 8080})
const clients = []
wss.on('connection', client => {
clients.push(client)
client.on('message', function(data) {
for(var c of clients){if(c !== client) {c.send(data)}}
})
client.on('close', function() {
clients.splice(clients.indexOf(client), 1)
})
})
// 运行执行:node server.js
// index1.html
const ws = new WebSocket('ws://localhost:8080')
btn.onclick = function() {ws.send('test')}
window.onbeforeunload = function() {ws.close()}
// index2.html
const ws = new WebSocket('ws://localhost:8080')
ws.onopen = function(e) {
ws.onmessage = function(e) {console.log(e.data)}
}
window.onbeforeunload = function() {ws.close()}
案例知识
- 歌词滚动
html 标签boolean属性可以直接写属性名,即为true
lorem 乱数假文
1
2
3lorem 随机生成一段话
lorem4 随机生成4个单词
li*30>lorem4 生成30个li,每个里面的内容是4个随机单词行盒(如span)、块盒(如div)
字体大小变化推荐使用transform控制性能更好,(因为transform是合成线程处理的)
1
2transform: scale(1.2);
transition: 0.2s; // 过渡时间
购物车
通过class 将业务逻辑抽离封装
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class Ui {
constructor() {
this.goodsData = new GoodsData();
this.uiData = new UiData();
}
}
class GoodsData{
constructor() {
}
}
class UiData {
constructor() {
}
}水波纹效果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// html
<div class="water-animate">test animate</div>
// css
.water-animate {
animation: water 500ms ease-in-out;;
}
@keyframes water {
0% {transform: scale(1);}
25% {transform: scale(0.8);}
50% {transform: scale(1.1);}
75% {transform: scale(0.9);}
100% {transform: scale(1.1);}
}抛物线图标移动效果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22// html
<div id="wrp" class="wrp-css">
<img id="icon" class="icon-css" src="xxx.png" />
</div>
// js
// 父层
wrp.style.transform = 'translateX(start)';
wrp.clientWidth // 强制渲染
wrp.style.transform = 'translateX(end)';
// 子层icon
icon.style.transform = 'translateY(start)';
icon.style.transform = 'translateY(end)';
// css
.wrp-css{
transition: 1s linear;
}
.icon-css{
transition: 1s cubic-bezier(0.5, -0.5, 1,1); // 抛物线
}属性描述符
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21class Person {
constructor(_data) {
// 属性描述符用于设置对象属性的各种规则,以确保对象属性在使用时的行为符合预期
Object.defineProperties(this, 'data', {
value: _data,
freeze: true, // 冻结(不可更改),默认false
writable: true, // 可重写,默认true
configurable: false, // 不可重新定义
enumerable: true, // 可遍历,默认true
get: function() {return this.data;}
set: function(value) {
if(typeof value !== 'object' ) {
throw new Error('value must be object')
}
this.data = value;
}
})
Object.seal(this); // 对象不可增加属性
Object.freeze(this); // 对象被冻结
}
}