🐬浏览器
# 🐬浏览器
# 浏览器知识体系
浏览器这里我的知识储备不太够!有空慢慢给填补上!
- 浏览器安全
- 浏览器中的进程&线程
- 浏览器的本地存储
- 浏览器的缓存
- 浏览器组成
- 浏览器渲染原理
- 事件循环机制
- 垃圾回收机制
# 浏览器安全
# 跨站脚本(XSS)攻击
# 浏览器灵魂之问 第7篇: 说一说XSS攻击? (opens new window)
XSS
全称是 Cross Site Scripting
(即跨站脚本
),为了和 CSS 区分,故叫它XSS
。XSS 攻击是指浏览器中执行恶意脚本(无论是跨域还是同域),从而拿到用户的信息并进行操作。
# 跨站请求伪造(CSRF)攻击
# 浏览器灵魂之问 第8篇: 说一说CSRF攻击?? (opens new window)
CSRF(Cross Site request forgery
), 即跨站请求伪造,指的是黑客诱导用户点击链接,打开黑客的网站,然后黑客利用用户目前的登录状态发起跨站请求。
# 浅谈CSRF攻击方式 - hyddd - 博客园 (cnblogs.com) (opens new window)
本文的例子举得很好(银行转账~) 图也给得很好理解~
CSRF的防御可以从服务端和客户端两方面着手,防御效果是从服务端着手效果比较好,现在一般的CSRF防御也都在服务端进行。
- 服务端的CSRF方式方法很多样,但总的思想都是一致的,就是在客户端页面增加伪随机数。
- Cookie Hashing(所有表单都包含同一个伪随机值):攻击者不能获得第三方的Cookie(理论上)
- 可以加验证码
# 浏览器引擎
# 浏览器中的进程与线程
# 浏览器渲染进程有哪些线程?
- JS(引擎)线程(主线程)——执行栈顶的代码
- GUI线程——负责绘制HTML页面的JS线程👆和GUI线程会互相等待(满足“忙则等待”原则)
- 事件监听线程——负责监听各种事件
- 计时线程——负责定时器相关
- 网络线程——负责各种网络请求的发送
# 浏览器本地存储
# 浏览器缓存
# 强缓存&强缓存相关字段
# Cache-Control字段
# 协商缓存&协商缓存相关字段
# 浏览器组成
# 浏览器内核的概念
浏览器内核(Rendering Engine),是指浏览器最核心的部分,负责对网页语法的解释(如标准通用标记语言 (opens new window)下的一个应用HTML (opens new window)、JavaScript (opens new window))并渲染(显示)网页。
所以,通常所谓的浏览器内核也就是浏览器所采用的渲染引擎 (opens new window),渲染引擎决定了浏览器如何显示网页的内容以及页面的格式信息。不同的浏览器内核对网页编写语法的解释也有不同,因此同一网页在不同的内核的浏览器里的渲染(显示)效果也可能不同,这也是网页编写者需要在不同内核的浏览器中测试网页显示效果的原因。
# 浏览器渲染原理
【7000字】一晚上爆肝浏览器从输入到渲染完毕原理 - 掘金 (juejin.cn) (opens new window)
# 浏览器的渲染过程-解析、绘制、合成
要了解浏览器页面的渲染过程,首先得知道
关键渲染路径
。关键渲染路径过程:
- 浏览器最初接收请求得到HTML、CSS、JS等资源
- 然后**解析、构建、渲染、布局、绘制、合成**
- 最后呈现在用户眼前界面。
# 解析文件
【1】解析 HTML 构建DOM树
【2】解析CSS 构建CSSOM树
【3】将DOM树和CSSOM树合并生成渲染树(渲染树的节点即为“渲染对象”)
【1】HTML文档描述一个页面的结构,浏览器通过
HTML解析器
将HTML解析成DOM树
结构。HTML文档中所有内容皆为节点,各节点间拥有层级关系,彼此相连,构成DOM树。
构建
DOM树
的过程:读取HTML文档的字节(Bytes)->将字节转换成字符(Chars)->依据字符确定标签(Tokens)->将标签转换成节点(Nodes)->以节点为基准构建DOM树。【2】CSS文档描述一个页面的表现,浏览器通过
CSS解析器
将CSS解析成CSSOM树
结构,与DOM树结构比较像。CSS文档中所有内容皆为节点,与HTML文档中的节点一一对应,各节点间拥有层级关系,彼此相连,构成CSSOM树。(和HTML文档内容的特点也完全相同(且一一对应))
构建
CSSOM树
的过程:与DOM树的构建过程👆完全一致。
【attention】在构建DOM树的过程中,当
HTML解析器
遇到<script>
时会立即阻塞DOM树的构建,将控制权移交给浏览器的JS引擎
,等到JS引擎
运行完毕,浏览器才会从中断的地方恢复DOM树的构建。<script>
的脚本加载完成后,JS引擎
通过DOM API
和CSSOM API
操作DOM树和CSSOM树。为何会产生渲染阻塞呢?其根本原因在于:JS操作DOM后,浏览器无法预测未来DOM的具体内容,为了防止无效操作和节省资源,只能阻塞DOM树的构建。
script 标签最好放在底部,不过也可以给 script 标签添加 defer 或者 async 属性让脚本的加载成为并行的,当然脚本的执行依旧需要是并发的~(👇绿色:文档的解析;蓝色:脚本的加载;红色:脚本的执行)
【3】浏览器的
渲染引擎
将DOM树和CSSOM树合并生成渲染树,只渲染需显示的节点及其样式。DOM树、CSSOM树和渲染树三者的构建并无
先后条件
和先后顺序
,并非完全独立而是会有交叉并行构建的情况。因此==会形成一边加载(脚本),一边解析(文档),一边渲染的工作现象==。
# 绘制图层
【4】根据渲染树生成布局渲染树(回流
)
【5】根据布局渲染树生成绘制渲染树(重绘
)
进入绘制阶段,遍历渲染树,调用渲染器的
paint()
在屏幕上绘制内容。根据渲染树布局计算样式,即每个节点在页面中的布局、尺寸等几何属性。HTML默认是流式布局,CSS和JS会打破这种布局,改变DOM的几何属性和外观属性。在绘制过程中,【4】根据渲染树布局,【5】再根据布局绘制,这就是回流重绘——
回流:几何属性需改变的渲染
可理解成,将整个网页填白,对内容重新渲染一次。只不过以人眼的感官速度去看浏览器回流是不会有任何变化的,若你拥有
闪电侠
的感官速度去看浏览器回流(实质是将时间调慢
),就会发现每次回流都会将页面清空,再从左上角第一个像素点从左到右从上到下这样一点一点渲染,直至右下角最后一个像素点。每次回流都会呈现该过程,只是感受不到而已。渲染树的节点发生改变,影响了该节点的几何属性,导致该节点位置发生变化,此时就会触发浏览器回流并重新生成渲染树。
重绘:更改外观属性而不影响几何属性的渲染
重绘指更改
外观属性
而不影响几何属性
的渲染。相比回流,重绘在两者中会温和一些渲染树的节点发生改变,但不影响该节点的几何属性。此时需要重新渲染对应节点的外观属性,触发浏览器重绘。
由此可见,回流对浏览器性能的消耗是高于重绘的,而且回流一定会伴随重绘,重绘却不一定伴随回流。
当生成渲染树后,至少会渲染一次。在后续交互过程中,还会不断地重新渲染。这时只会
回流重绘
或只有重绘
。因此引出一个定向法则:回流必定引发重绘,重绘不一定引发回流。
# 合成图层
【6】根据绘制渲染树合成图层后显示在屏幕上
将回流重绘生成的图层逐张合并并显示在屏幕上。上述几个步骤并不是一次性顺序完成的,若DOM或CSSOM被修改,上述过程会被重复执行。实际上,CSS和JS往往会多次修改DOM或CSSOM,简单来说就是用户的交互操作引发了网页的重渲染。
总的来说——记住这张图:
完整过程中的一些细节——
- 首先解析收到的文档,根据文档定义构建一棵 DOM 树,DOM 树是由 DOM 元素及属性节点组成的。
- 然后对 CSS 进行解析,生成 CSSOM 规则树。
- 根据 DOM 树和 CSSOM 规则树构建渲染树。渲染树的节点被称为渲染对象,渲染对象是一个包含有颜色和大小等属性的矩形,渲染对象和 DOM 元素相对应,但这种对应关系不是一对一的,不可见的 DOM 元素不会被插入渲染树。还有一些 DOM元素对应几个可见对象,它们一般是一些具有复杂结构的元素,无法用一个矩形来描述。
- 当渲染对象被创建并添加到树中,它们并没有位置和大小,所以当浏览器生成渲染树以后,就会根据渲染树来进行布局(也可以叫做回流)。这一阶段浏览器要做的事情是要弄清楚各个节点在页面中的确切位置和大小。通常这一行为也被称为“自动重排”。
- 布局阶段结束后是绘制阶段,遍历渲染树并调用渲染对象的 paint 方法将它们的内容显示在屏幕上,绘制使用 UI 基础组件。
# 【细节】解析文档(HTML)过程中,是将HTML都解析完了再去生成渲染树麽?
**注意:**这个过程是逐步完成的,为了更好的用户体验,渲染引擎将会尽可能早的将内容呈现到屏幕上,并不会等到所有的html 都解析完成之后再去构建和布局渲染树。它是解析完一部分内容就显示一部分内容,同时,可能还在通过网络下载其余内容。
DOM树、CSSOM树和渲染树三者的构建并无
先后条件
和先后顺序
,并非完全独立而是会有交叉并行构建的情况。因此==会形成一边加载(脚本),一边解析(文档),一边渲染的工作现象==。
# 【细节】script中的脚本文件、link中的css文件的解析/执行会阻塞文档解析麽?如何阻塞?
前者会;后者不会√
- 脚本的加载会阻塞文档解析
如果没有 defer 或 async 属性,浏览器会立即加载并执行相应的脚本。它不会等待后续加载的文档元素,读取到就会开始加载和执行,这样就阻塞了后续文档的加载。
下图可以直观的看出三者之间的区别:
所以script要放在底部/加async defer关键字
拓展学习:
JavaScript 的加载、解析与执行会阻塞文档的解析,也就是说,在构建 DOM 时,HTML 解析器若遇到了 JavaScript,那么它会暂停文档的解析,将控制权移交给 JavaScript 引擎,等 JavaScript 引擎运行完毕,浏览器再从中断的地方恢复继续解析文档。
也就是说,如果想要首屏渲染的越快,就越不应该在首屏就加载 JS 文件,这也是都建议将 script 标签放在 body 标签底部的原因。
- 当然在当下,并不是说 script 标签必须放在底部,因为你可以给 script 标签添加 defer 或者 async 属性。
- CSS的解析 不会阻塞DOM文档解析
理论上,既然样式表不改变 DOM 树,也就没有必要停下文档的解析等待它们。
因为 JavaScript 脚本执行时可能在文档的解析过程中请求样式信息(比如根据样式获取元素节点),如果样式还没有加载和解析,脚本将得到错误的值,显然这将会导致很多问题。所以——要把script放在底部/加async defer关键字
# 【细节】什么情况会阻止浏览器渲染
前面问的细节,这个问题比较全面了就
要明确——首先渲染的前提是生成渲染树
- 所以 HTML 和 CSS 肯定会阻塞渲染。
如果你想渲染的越快,你越应该降低一开始需要渲染的文件大小,并且扁平层级,优化选择器。
- 浏览器在解析到 script 标签时,会暂停构建 DOM,完成后才会从暂停的地方重新开始。
也就是说,如果你想首屏渲染的越快,就越不应该在首屏就加载 JS 文件,这也是都建议将 script 标签放在 body 标签底部的原因。
拓展知识
并不是说 script 标签必须放在底部,因为你可以给 script 标签添加 defer 或者 async 属性
- 当 script 标签加上 defer 属性以后,表示该 JS 文件会并行下载,但是会放到 HTML 解析完成后顺序执行,所以对于这种情况你可以把 script 标签放在任意位置。
- 同理,对于没有任何依赖的 JS 文件可以加上 async 属性,表示 JS 文件下载和解析不会阻塞渲染(async属性不能保证JS文件的执行是按顺序的~)。
记住下面这个图就好~
蓝色代表 js 脚本网络加载时间,红色代表 js 脚本执行时间,绿色代表 html 解析
# 回流(重排)与重绘
前端知识库@JD
# 什么是回流 & 重绘
回流(生成渲染树那一步):当渲染树中的一部分(或全部)因为元素的尺寸、布局、显隐发生改变而需要重新构建,就是回流。回流后会进行重绘(上图中 绘制那一步)👇。
- 添加或者删除可见的 DOM 元素
- 元素的位置发生改变
- 元素的尺寸发生改变--边距、填充、边框、宽高
- 内容改变--比如文本改变或者图片大小改变而引起的计算值宽高的改变
- 页面初始化渲染
- 浏览器窗口尺寸改变
重绘:当只是元素的外观、风格变化,不影响布局的,重新渲染的过程就叫重绘。
回流必将引起重绘,而重绘不一定会引起回流。每个页面至少回流一次,就是在页面第一次加载的时候。
# 如何减少回流和重绘
# 将频繁回流重绘的节点设置为图层
上一章的
渲染过程
最后一步,提到将回流重绘生成的图层逐张合并并显示在屏幕上。可将其理解成Photoshop
的图层,若不对图层添加关联,图层间是不会互相影响的。同理,在浏览器中设置频繁回流或重绘的节点为一张新图层,那么新图层就能够阻止节点的渲染行为影响别的节点,这张图层里怎样变化都无法影响到其他图层。设置新图层有两种方式,将节点设置为
<video>
或<iframe>
,为节点添加will-change
。will-change
是一个很叼的属性!使用
cssText
或者className
一次性改变属性
- 避免频繁使用 style,而是采用修改
class
的方式。使用 document fragment
- 用
createDocumentFragment
进行批量的 DOM 操作。对于多次重排的元素,如动画,使用绝对定位脱离文档流,使其改变不影响其他元素。
对于 resize、scroll 等进行防抖/节流处理。
# 避免使用Table布局
牵一发而动全身
用在Table布局身上就很适合了,可能很小的一个改动就会造成整个<table>
回流。通常可用
<ul>
、<li>
和<span>
等标签取代<table>
系列标签生成表格。# 避免规则层级过多
浏览器的
CSS解析器
解析css文件
时,对CSS规则是从右到左匹配查找,样式层级过多会影响回流重绘效率,建议保持CSS规则在3层
左右。# 使用visibility:hidden替换display:none
display:none
简称DN
,visibility:hidden
简称VH
。
- 占位表现
- DN不占据空间
- VH占据空间
- 触发影响
- DN触发回流重绘
- VH触发重绘
- 过渡影响
- DN影响过渡不影响动画
- VH不影响过渡不影响动画
- 株连效果
- DN后自身及其子节点全都不可见
- VH后自身及其子节点全都不可见但可声明子节点
visibility:visible
单独显示两者的
占位表现
、触发影响
和株连效果
就能说明VH
代替DN
的好处,从两者区别中就能找出恰当的答案了。# 使用transform代替top(绝对定位)
top
是几何属性,操作top
会改变节点位置从而引发回流,使用transform:translate3d(x,0,0)
代替top
,只会引发图层重绘,还会间接启动GPU加速添加
will-change: tranform
,让渲染引擎为其单独实现一个图层,当这些变换发生时,仅仅只是利用合成线程去处理这些变换,而不牵扯到主线程,大大提高渲染效率。当然这个变化不限于tranform
, 任何可以实现合成效果的 CSS 属性都能用will-change
来声明。这里有一个实际的例子,一行will-change: tranform
拯救一个项目,点击直达 (opens new window)。# 避免节点属性值放在循环里当成循环变量
for (let i = 0; i < 10000; i++) { const top = document.getElementById("css").style.top; console.log(top); }
每次循环操作DOM都会发生回流!应该在循环外使用变量保存一些不会变化的DOM映射值。
const top = document.getElementById("css").style.top; for (let i = 0; i < 10000; i++) { console.log(top); }
# 回流-几何属性,重绘-外观属性
以下对一些常用的几何属性和外观属性分类,其实同种分类的属性都有一些共同点,各位同学可自行感受。推荐一个查询属性渲染状态的网站CssTriggers (opens new window),可查看每个属性在渲染过程中发生了什么影响了什么。
Layout-布局;Paint-绘制;Composite-合成
- 几何属性:包括布局、尺寸等可用数学几何衡量的属性——改变几何属性会导致回流!
- 布局:
display
、float
、position
、list
、table
、flex
、columns
、grid
- 尺寸:
margin
、padding
、border
、width
、height
- 外观属性:包括界面、文字等可用状态向量描述的属性——改变外观属性会导致重绘!
- 界面:
appearance
、outline
、background
、mask
、box-shadow
、box-reflect
、filter
、opacity
、clip
- 文字:
text
、font
、word
# 浏览器同源策略(跨域问题的解决)
# 浏览器拒绝跨域的读操作,允许跨域写操作、跨域资源嵌入
严格的说,浏览器并不是拒绝所有的跨域请求,实际上拒绝的是跨域的读操作。浏览器的同源限制策略是这样执行的:
- 通常浏览器允许进行跨域写操作(Cross-origin writes),如链接,重定向;
- 通常浏览器允许跨域资源嵌入(Cross-origin embedding),如 img、script 标签;
- 通常浏览器不允许跨域读操作(Cross-origin reads)。
# 浏览器事件机制
# 微任务、宏任务与Event-Loop (opens new window)
又看到篇好文~
# JS引擎的执行机制——事件循环 Event Loop
看了下这篇文章 简单入门10分钟理解JS引擎的执行机制 (opens new window)
注意本文中所有执行流程是基于浏览器环境,而不是node环境
node轮询有phase(阶段)的概念 浏览器和NodeJS中不同的Event Loop (opens new window)
事件循环的核心机制是:宏任务、微任务及其相关队列的执行流程图
单线程的JS通过事件循环 Event Loop 实现异步
JS的执行机制是
- 首先判断JS是同步还是异步,同步就进入主线程,异步就进入event table
- 异步任务在event table中注册函数,当满足触发条件后,被推入event queue
- 同步任务进入主线程后一直执行,直到主线程空闲时,才会去event queue中查看是否有可执行的异步任务,如果有就推入主线程中
以上三步循环执行,这就是event loop
举个例子
console.log(1) setTimeout(function(){ console.log(2) },0) console.log(3)
1.console.log(1) 是同步任务,放入主线程里 2.setTimeout() 是异步任务,被放入event table, 0秒之后被推入event queue里 3.console.log(3) 是同步任务,放到主线程里 // 当 1、 3在控制条被打印后,主线程去event queue(事件队列)里查看是否有可执行的函数,执行setTimeout里的函数
但这还不够!
# 微任务、宏任务的概念
如果有多个任务在event queue里呆着呢?谁先?谁后?上新概念!
上道题
setTimeout(function(){
console.log('定时器开始啦')
});
new Promise(function(resolve){
console.log('马上执行for循环啦');
for(var i = 0; i < 10000; i++){
i == 99 && resolve();
}
}).then(function(){
console.log('执行then函数啦')
});
console.log('代码执行结束');
尝试按照,上文我们刚学到的JS执行机制去分析
【1】setTimeout 是异步任务,被放到event table
【2】new Promise 是同步任务,被放到主线程里,直接执行打印 console.log('马上执行for循环啦')
【3】.then里的函数是 异步任务,被放到event table
【4】 console.log('代码执行结束')是同步代码,被放到主线程里,直接执行
所以,结果是 【马上执行for循环啦 --- 代码执行结束 --- 定时器开始啦 --- 执行then函数啦】吗?
亲自执行后,结果居然不是这样,而是【马上执行for循环啦 --- 代码执行结束 --- 执行then函数啦 --- 定时器开始啦】
欸?不是setTimeout这个任务先进的event table麽?
并不是!上述根据异步同步一股脑划分的方法不对!
而准确的划分方式是:
- macro-task(宏任务):包括
script
脚本的执行;setTimeout
,setInterval
一类的定时事件;I/O操作,UI渲染 - micro-task(微任务):
Promise
,process.nextTick
(Node独有)
按照这种分类方式:JS的执行机制是
- 【1】执行一个宏任务(JS脚本中的内容都是宏任务~),过程中如果【2】遇到微任务,就将其【3】放到微任务的【事件队列】里
- 当前【4】宏任务执行完成后,会查看微任务的【事件队列】,并【5】将里面全部的微任务依次执行完
重复以上2步骤,结合event loop(1) event loop(2) ,就是更为准确的JS执行机制了。
尝试按照刚学的执行机制,去分析例2:
1.首先执行script下的宏任务,遇到setTimeout,将其放到宏任务的【队列】里
2.遇到 new Promise直接执行,打印"马上执行for循环啦"
3.遇到then方法,是微任务,将其放到微任务的【队列里】
4.打印 "代码执行结束"
5.本轮宏任务执行完毕,查看本轮的微任务,发现有一个then方法里的函数, 打印"执行then函数啦"
6.到此,本轮的event loop 全部完成。
7.下一轮的循环里,先执行一个宏任务,发现宏任务的【队列】里有一个 setTimeout里的函数,执行打印"定时器开始啦"
所以最后的执行顺序是【马上执行for循环啦 --- 代码执行结束 --- 执行then函数啦 --- 定时器开始啦】
# 谈谈setTimeout
这段setTimeout代码什么意思? 我们一般说: 3秒后,会执行setTimeout里的那个函数
setTimeout(function(){
console.log('执行了')
},3000)
但是这种说并不严谨,准确的解释是: 3秒后,setTimeout里的函数会被推入event queue,而event queue(事件队列)里的任务,只有在主线程空闲时才会执行。
所以只有满足 (1)3秒后 (2)主线程空闲,同时满足时,才会3秒后执行该函数
如果主线程执行内容很多,执行时间超过3秒,比如执行了10秒,那么这个函数只能10秒后执行了
# 事件循环经典例题
# async-await后的语句在微任务中的位置
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
function async2() {
console.log('async2 start')
return new Promise((resolve) => {
resolve(123)
console.log('async2 promise')
}).then((res) => {
console.log(res);
})
}
console.log('script start')
setTimeout(function () {
console.log('setTimeout')
}, 0)
async1()
new Promise(function (resolve) {
console.log('promise1')
resolve()
})
.then(function () {
console.log('promise2')
})
.then(function () {
console.log('promise3')
})
console.log('script end')