浏览器
浏览器内核
- 支撑浏览器运行的最核心程序。
- 内核由很多模块组成。
(主线程)
JS引擎:负责JS程序的编译与运行。
HTML解析器,CSS解析器:负责页面文本的解析。
布局和渲染模块:负责页面的布局和效果的绘制。
DOM/CSS模块:负责DOM/CSS在内存中的相关处理。
(分线程)
定时器模块:负责定时器的管理。
DOM事件相应模块:负责事件的管理。
网络请求模块:负责ajax请求。
- 常见浏览器内核
Trident内核:IE、360和搜狗等国内浏览器。
Gecko内核:firefox。
Webkit内核:Safari。
Blink内核:Chorm(Chromium=>Blink)
Presto内核:Opera(2013年2月宣布放弃=>Chromium=>Blink)。
浏览器渲染过程
HTML解析器
- 解析HTML标签以及内部样式,并构建DOM树。
CSS解析器
- 解析外联样式,并构建CSS树。
HTML解析器和CSS解析器将DOM树和CSS树交给JS引擎。(DOMContentLoaded)
JS引擎
- 处理JS代码,根据JS代码完成绑定事件、修改DOM树和CSS树等操作。
- 先执行初始化代码(设置定时器、绑定事件监听、发送ajax请求),再执行回调代码。
布局模块(layout)
- 将修改后的DOM树和CSS树合并成一个渲染树,得到节点的几何信息,交给绘图模块。
浏览器
- 请求图片、音频等资源交给绘图模块。
绘图模块(painting)
- 根据渲染树和资源,得到节点的绝对像素,完成绘图。(onload)
CSS阻塞
- style标签中的样式:由HTML解析器进行解析,不会阻塞浏览器渲染(可能会产生“闪屏现象”),不会阻塞DOM解析
- link引入的CSS样式:由CSS解析器进行解析,会阻塞浏览器渲染,会阻塞后面的js语句执行,不阻塞DOM的解析
优化
- 使用CDN节点进行外部资源加速。
- 对外部样式文件进行压缩(使用打包工具)。
- 优化样式文件代码。
JS阻塞
JS会阻塞后续DOM的解析,会阻塞页面渲染,会阻塞后续JS的执行。
优化
<script async></script>
加上async属性,JS代码会异步加载并执行(所有script标签, 谁先加载完谁执行)。<script defer></script>
加上defer属性,JS代码会异步加载(所有script标签按顺序在DOM节点加载完后执行)- 把script标签放在页面尾部。
- 优化JS代码。
资源加载阻塞
无论是CSS阻塞,还是JS阻塞,都不会阻塞浏览器加载外部资源。浏览器始终处于一种”先把请求发出去“的工作模式,只要是涉及到网络请求的内容,都会先发送请求去获取资源,至于资源到本地后什么时候用,由浏览器自己协调。
重绘回流
重绘
- 节点需要改变外观,而不会影响布局。
- 如:
改变颜色、背景颜色。
回流
- 布局或几何属性发生改变。
- 如:
页面首次渲染;
浏览器窗口大小发生改变(resize事件);
元素尺寸或位置发生改变;
元素内容改变(文字数量或图片大小);
元素字体大小改变(font-size);
添加或删除可见的DOM元素;
激活CSS伪类(hover);
重绘不一定会发生回流,回流一定会发生重绘。
减少重绘回流
- 合并css修改样式,采用class来修改。
- 避免使用table布局,可能一个小的改动会造成整个table的重新布局。
- 使用visibility代替display。
- 把DOM离线(display:none)后再修改,修改后再显示。
- 使CSS3硬件加速,如transform、 opacity、filters这些属性。
- 脱离文档流(float、绝对定位absolute、固定定位fixed)。
Event Loop中的渲染UI
- 当 Event loop 执行完 Microtasks 后,会判断 document 是否需要更新。因为浏览器是 60Hz 的刷新率,每 16ms 才会更新一次。
- 然后判断是否有
resize
或者scroll
,有的话会去触发事件,所以resize
和scroll
事件也是至少 16ms 才会触发一次,并且自带节流功能。- 判断是否触发了 media query。
- 更新动画并且发送事件。
- 判断是否有全屏操作事件。
- 执行
requestAnimationFrame
回调。- 执行
IntersectionObserver
回调,该方法用于判断元素是否可见,可以用于懒加载上,但是兼容性不好。- 更新界面。
- 以上就是一帧中可能会做的事情。如果在一帧中有空闲时间,就会去执行
requestIdleCallback
回调。
Event Loop
https://juejin.cn/post/6868849475008331783#heading-14
- 代码开始执行,script标签作为宏任务执行。
- 执行过程,遇到同步代码就执行,遇到微任务放入微任务队列,遇到宏任务放入宏任务队列。
- 执行微任务队列。过程中遇到微任务队列放入微任务队列,遇到宏任务放入宏任务队列。在此过程中产生的微任务会一并清空。
- 渲染UI。
- 执行宏任务队列。过程中遇到微任务队列放入微任务队列,遇到宏任务放入宏任务队列。在此过程中产生的微任务会一并清空。
Promise的
executor
是同步的。
宏任务、微任务
宏任务
- script
- setTimeout的回调函数
- setInterval的回调函数
- I/O
- ajax的回调函数
- setImmediate的回调函数(node)
微任务
- promise.then().catch().finally()
- MutationObserver
- Object.observe
- process.nextTick(node)
DOM事件流
1、事件捕获阶段(从上往下)
2、目标阶段
3、事件冒泡阶段(从下往上)
addEventListener
//第三个参数:false(冒泡,默认)、true(捕获)
事件代理(委托)
如果一个节点中的子节点是动态生成的,那么子节点需要注册事件的话应该注册在父节点上。
优点
- 绑定在父节点上只需要绑定一次,节省内存。
- 不需要一个个给子节点绑定事件。
进程和线程
进程是除了CPU之外系统资源分配的基本单位。
线程是CPU调度分配的基本单位。
JS是单线程还是多线程?
- JS是单线程的。比如定时器的回调函数只有等运行栈中的代码全部执行完后才有可能执行。
但是使用H5中的Web Workers可以多线程运行。
为什么JS要用单线程,而不用多线程?
- 作为浏览器脚本语言,JS的主要用途是与用户互动以及操作DOM,这就决定了它只能是单线程,否则会带来复杂的同步问题。
浏览器运行是单进程还是多进程?
- 有的是单进程,有的是多进程。
Session、Token、JWT
Session
- 用户数据存储在服务器。相当于浏览器发送SessionID(一种cookie)给服务器,服务器查询是否符合,如果符合就根据数据库里对应Session对象的用户数据去进行操作。
- 支持存储任何类型的数据。
- 有效时间短(客户端关闭或者会话时间结束失效)。
- 只能用在
单个节点的域
或者它的子域
中有效。- 占用服务器资源。
Token
- 服务器收到token后需要查库验证token是否有效。
- uid+时间戳+签名
JWT(Json Web Token)
- header.payload.signiture
header:指定加密算法。(alg表示签名的加密算法;typ表示token的类型)
payload:签名信息+用户数据。
signature:数字签名。header+payload用base64URL算法处理后的字符串+指定的加密算法+服务器密钥=>数字签名;- 服务器端只需要用服务器密钥解密即可得到用户数据,无需查询数据库。
- 可以跨域使用(因为可以放在header里面,而cookie和session是放在response里)。
浏览器缓存
强制缓存
- 优先级:Cache-Control > Expires
- Expires
在HTTP/1.0时期使用,存在坑(服务器时间和浏览器时间可能不一致)遂被弃用。- Cache-Control
在HTTP/1.1时期使用。
max-age:设置有效时间。
public:不仅浏览器缓存,中间的代理服务器也进行缓存。
private:只有浏览器缓存。
no-cache:跳过当前的强缓存,发送HTTP请求(进入协商缓存阶段)。
no-store:不进行任何形式的缓存。
s-maxage:针对代理服务器的缓存时间。
强制缓存失效时,浏览器在请求头中携带缓存tag向服务器发请求,由服务器决定是否使用缓存。
协商缓存
- 精准度:Etag > Last-Modified
- 性能:Last-Modified > Etag
- Last-Modified / If-Modified-Since
- Etag / If-None-Match
缓存策略
- 对于某些不需要缓存的资源:Cache-control: no-store
- 对于频繁变动的资源:Cache-control: no-store和Etag
- 对于代码文件:Cache-control: max-age和Etag
Cookie、localStorage、sessionStorage、IndexedDB
Cookie
- 为了解决HTTP无状态,进行状态管理。
- 参与与服务端通信。
- 缺陷
容量缺陷:只有4KB。
性能缺陷:Cookie紧跟域名,不管域名下的某个地址是否需要这个Cookie,请求都会带上完整的Cookie,容易造成性能浪费。
安全缺陷:在http-only为false时,容易发生XSS攻击。
//value:键值对,字符串类型。
//domain:指定所属域名,默认是当前域名。
//path:指定在哪个路径下生效,默认是/。
//maxAge:失效的时间,单位s。
//expires:过期时间。
//http-only:不能通过JS访问Cookie,减少XSS攻击。
//secure:是否仅当使用安全协议传输时使用,默认false。
//same-site:规定浏览器不能在跨域请求中携带Cookie,减少CSRF攻击。
document.cookie= "username=John Doe; expires=Thu, 18 Dec 2013 12:00:00 GMT;path=/";
localStorage
- 存储在本地,5M。
- 永久存储,永不失效。
- 应用
存储内容稳定的资源,如官网logo、base64格式图片资源。
let obj = { name: "a", age: 18};
localStorage.setItem("name", "b");
localStorage.setItem("obj", JSON.stringfy(obj));//注意字符串
let name = localStorage.getItem("name");
let obj_p = JSON.parse(localStorage.getItem("obj"));
localStorage.removeItem("name");
localStorage.clear();
sessionStorage
- 存储在本地,5M。
- 页面关闭后就失效。
- 应用
表单信息维护,保证刷新后不会丢失内容。
存储本次浏览记录。
IndexedDB
- 运行在浏览器的非关系型数据库。
- 理论上容量没有上限。
- 键值对存储。
- 异步操作。
- 受同源策略影响,无法访问跨域的数据库。
Web Worker
注意点
- 同源限制:分配给Worker线程运行的脚本文件,必须与主线程的脚本文件同源。
- DOM限制:Worker线程无法使用document、window、parent这些对象,但是可以使用navigator、location对象。
- 通信联系:Worker线程和主线程不在同一个上下文环境,不能直接通信,必须通过消息完成。
- 脚本限制:不能执行alert()和confirm()方法,但是可以使用XMLHttpRequest对象发出AJAX请求。
- 文件限制:无法读取本地文件,所加载的脚本必须来自网络。
//主线程.js
//开启Worker:来源只能是网络文件。
var worker = new Worker('Worker线程.js');
//向Worker发消息
worker.postMessage('hello');
//接受Worker的信息
worker.onmessage = function(event){
console.log(event);
}
//监听错误
worker.onerror(function (event) {
console.log([
'ERROR: Line ', e.lineno, ' in ', e.filename, ': ', e.message
].join(''));
});
//关闭Woker
worker.terminate();
//Worker线程.js
//接受主线程的信息
self.addEventListener('message', function(e){
//self代表子线程自身,也可不写,也可写成this。
//向主线程发消息。
self.postMessage(e);
//关闭自身。
self.close();
}, false);
//加载其他脚本
importScripts('j1.js', 'j2.js');
实现浏览器内多个标签页之间的通信
WebSocket协议
- 因为HTTP通信只能由客户端发起,服务器无法主动向客户端发送消息。当服务器有变化时,只能通过客户端不断地轮询访问来获取消息。而WebSocket是全双工通信,则可以实现多个标签页之间的通信。
localStorage
sharedWorker
- 同源窗口下。
// page.js
let myWorker = new SharedWorker('worker.js');
// page通过worker port发送消息
myWorker.port.postMessage('哼哼');
// page通过worker port接收消息
myWorker.port.onmessage = (e) => console.log(e.data);
// worker.js
onconnect = function(e) {
const port = e.ports[0];
port.postMessage('哈嘿');
port.onmessage = (e) => {
console.log(e.data);
}
}