-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
笔记之浏览器工作原理与实践 #65
Labels
Comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
浏览器工作原理与实践
本文是对李兵老师的《浏览器工作原理与实践》课程的记录与整理。
课程地址:浏览器工作原理与实践
一、 宏观视角下的浏览器
1. Chrome 多进程架构
chrome 基于 Chromium 开发
线程 VS 进程
IPC
通信(IPC
负责进程间通信)单进程浏览器时代
单进程:单进程浏览器是指浏览器的所有功能模块都是运行在同一个进程里
页面进程(包含页面渲染,JavaScript 环境,插件)
多进程浏览器时代
1.早期架构(通过 IPC)
当前正在使用的多线程架构
未来面向服务架构
补充
2. 协议
在衡量 Web 页面性能的时候有一个重要的指标叫“FP(First Paint)”,是指从页面加载到首次开始绘制的时长。
1. IP:将数据包送达目的主机(网络层)
2. UDP:将数据包送达应用程序(传输层)
3. TCP:数据完整送达应用程序(传输层)
TCP 为了保证数据传输的可靠性,牺牲了数据包的传输速度,因为“三次握手”和“数据包校验机制”等把传输过程中的数据包的数量提高了一倍。
3. Http 请求流程
构建请求
GET /index.html HTTP1.1
查找缓存
浏览器命中缓存时,会拦截请求,返回副本
准备 IP 地址和端口(DNS)
等待 TCP 队列
Chrome 机制:只能建立 6 个连接
建立 TCP 连接
发送 HTTP 请求(同时发送)
服务器处理 HTTP 请求流程
connection:Keep=Alive
保持 TCP 连接可以省去下次请求建立连接时间,提升加载速度两个问题
1、站点第二打开速度很快
2、登录态保持(cookie)
Set-Cookie: UID=3431uad;
Cookie: UID=3431uad;
4. 从输入 URL 到页面展示发生了什么
用户发出 URL 请求到页面开始解析的这个过程,就叫做导航。
用户输入
输入内容
使用浏览器默认搜索引擎,合成新的带搜索关键字 URL
请求 URL
自动加上协议,回车后标签图片进入加载状态,页面不会立即更换,等待文档提交阶段,页面内容才会变化
URL 请求过程(IPC 通信)
Content-Type:告诉浏览器返回的响应体数据的类型
准备渲染进程
同一站点,复用父页面渲染
https://
或者http://
),还包含了该根域名下的所有子域名和不同的端口提交文档(URL 请求响应体数据)
渲染阶段
5、渲染流程
主线程
构建 DOM 树
样式计算
css 结构-stylessheets
转换属性值标准化
计算 DOM 树每个节点具体样式
布局阶段(Layout Tree)
分层 图层树(LayerTree)
渲染引擎还需要为特定的节点升成专用的图片并生成图层树
拥有层叠 上下文属性元素会被单独升为一层
需要剪裁(clip)被创建为图层
overflow:auto
图层绘制(Paint)
绘制指令
非主线程(合成线程)
栅格化操作
合成线程会按照视口附近的图块来优先生成位图,实际生成位图的操作是由栅格化来执行的。所谓栅格化,是指将图块转换为位图
重排. 重绘. 合成
重排:更改了元素几何属性,需要更新整个渲染流水线,开销最大。
重绘:更改元素绘制属性,省去了布局和分层阶段,执行效率高一点
合成:渲染引擎将跳过布局和绘制,只执行后续的合成操作,我们把这个过程叫做合成。非主线程操作
二、 javascript 执行机制
6. JavaScript 变量提升
变量提升(Hoisting)
所谓的变量提升,是指在 JavaScript 代码执行过程中,JavaScript 引擎把变量的声明部分和函数的声明部分提升到代码开头的“行为”。变量被提升后,会给变量设置默认值,这个默认值就是我们熟悉的 undefined。
变量声明:变量提升,并且默认值是 undefined
函数声明
同名变量和函数规则
JavaScript 执行流程:先编译,再执行
编译阶段:变量和函数会放到变量环境中
执行上下文(Execution context)
执行上下文是 JavaScript 执行一段代码时的运行环境
可执行代码
执行阶段
JavaScript 引擎开始执行可执行代码,按照顺序一行一行执行
7. 调用栈
函数调用
运行一个函数,方式:函数名()
调用栈(执行上下文栈):管理函数调用的数据结构
调用栈是 JavaScript 引擎追踪函数执行的一个机制
如何利用浏览器查看调用栈信息:Call Stack
栈溢出(stack overflow):调用栈是有大小的
8. 块级作用域
作用域
作用域是指在程序中定义变量的区域,该位置决定了变量的生命周期。通俗地理解,作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期。
变量提升带来的问题
JavaScript 如何支持块级作用域
编译并创建执行上下文
继续执行代码,执行完毕,定义的变量就会从词法环境栈顶弹出
总结:块级作用域是通过词法环境的栈结构实现,变量提升是通过变量环境实现。
9. 作用域链与闭包
作用域链
词法作用域
词法作用域就是指作用域是由代码函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符。
词法作用域是代码阶段就决定好的,和函数是怎么调用的没有关系
块级作用域中的变量查找
查找过程为 1、2、3、4、 5
闭包
在 JavaScript 中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。比如外部函数是 foo,那么这些变量的集合就称为 foo 函数的闭包。
举栗子
10. this
全局执行上下文中 this
指向 window 对象
函数执行上下文中 this
默认情况下,调用函数 this 也是指向 window
设置函数执行上下文中的 this 值
函数 call,apply,bind 方法
对象调用方法设置
通过构造函数(new)
首先创建了一个空对象 tempObj;
接着调用 CreateObj.call 方法,并将 tempObj 作为 call 方法的参数,这样当 CreateObj 的执行上下文创建时,它的 this 就指向了 tempObj 对象;
然后执行 CreateObj 函数,此时的 CreateObj 函数执行上下文中的 this 指向了 tempObj 对象;
最后返回 tempObj 对象。
this 设计缺陷及方案
三、 V8 工作原理
11.栈空间与堆空间
JavaScript 语言类型(动态的弱类型)
静态语言:使用之前需要确定其变量数据类型的称为静态语言。
动态语言:运行过程中需要检查数据类型的称为动态语言。
弱类型语言:支持隐式类型转换的语言
强类型语言:不支持隐式类型转换
JavaScript 数据类型
原始类型
引用类型:Object
古老的坑,
typeof null ->Object
,为什么 "typeof null" 的结果为 "object" ?内存空间
原始类型的数据值都是直接保存在“栈”中的,引用类型的值是存放在“堆”中的。
再谈闭包
从内存模型的角度分析这段代码的执行流程
闭包的核心:1. 预扫描内部函数 2. 把内部函数引用的外部变量保存在堆中
12.垃圾回收
1.调用栈中数据回收(下移 ESP)
举栗子
执行函数时调用栈如图:
ESP 指针:记录当前执行状态的指针,ESP 下移操作就是销毁函数执行上下文的过程。
2.堆中数据回收(垃圾回收器)
代际假说,垃圾回收的基础
V8 中将堆分为新生代和老生代两个区域
新生代中存放的是生存时间短的对象,老生代中存放的生存时间久的对象。
新生区通常只支持 1 ~ 8M 的容量,而老生区支持的容量就大很多了。
垃圾回收器的工作流程
副垃圾回收器(新生区)
大多数小的对象都会被分配到新生区,所以说这个区域虽然不大,但是垃圾回收还是比较频繁的。
使用Scavenge 算法处理,是把新生代空间对半划分为两个区域,一半是对象区域,一半是空闲区域,新加入的对象都会存放到对象区域。
在垃圾回收过程中,首先要对对象区域中的垃圾做标记;标记完成之后,就进入垃圾清理阶段,副垃圾回收器会把这些存活的对象复制到空闲区域中,同时它还会把这些对象有序地排列起来,所以这个复制过程,也就相当于完成了内存整理操作,复制后空闲区域就没有内存碎片了。
完成复制后,对象区域与空闲区域进行角色翻转,也就是原来的对象区域变成空闲区域,原来的空闲区域变成了对象区域。这样就完成了垃圾对象的回收操作,同时这种角色翻转的操作还能让新生代中的这两块区域无限重复使用下去。
如果新生区空间设置得太大了,那么每次清理的时间就会过久,所以为了执行效率,一般新生区的空间会被设置得比较小。
新生去空间小,JavaScript 引擎采用了对象晋升策略,也就是经过两次垃圾回收依然还存活的对象,会被移动到老生区中。
主垃圾回收器(老生区)
老生区中的对象有两个特点,一个是对象占用空间大,另一个是对象存活时间长。
主垃圾回收器是采用**标记 - 清除(Mark-Sweep)**的算法进行垃圾回收。
标记阶段就是从一组根元素开始,递归遍历这组根元素,在这个遍历过程中,能到达的元素称为活动对象,没有到达的元素就可以判断为垃圾数据。
清除过程,清除标记为垃圾数据的过程
标记 - 整理(Mark-Compact),整理内存碎片
全停顿
增量标记(Incremental Marking)算法:降低老生代垃圾回收的卡顿,V8 将标记过程分为一个个的子标记过程,同时让垃圾回收标记和 JavaScript 应用逻辑交替进行,直到标记阶段完成
13. 编译器与解释器
编译器 Compiler(编译型语言)
编译型语言在程序执行之前,需要经过编译器的编译过程,并且编译之后会直接保留机器能读懂的二进制文件,这样每次运行程序时,都可以直接运行该二进制文件,而不需要再次重新编译了。例如 Java 和 GO
解释器 Interpreter(解释型语言)
解释型语言编写的程序,在每次运行时都需要通过解释器对程序进行动态解释和执行
V8 是如何执行一段 JavaScript 代码
生成抽象语法树
AST
和执行上下文生成
AST
阶段:先分词,在解析分词(tokenize),又称词法分析
作用是将一行行的源码拆解成一个个 token。所谓 token,是指语法上不可能在分,最小的单个字符或字符串
解析(parse),又称为语法分析
作用是将 token 数据,根据规则转换为
AST
。不符合语法规则会抛出语法错误。几种常见错误如下:EvalError
:代表了一个关于 eval 函数的错误.此异常不再会被 JavaScript 抛出,但是 EvalError 对象仍然保持兼容性RangeError
:越界错误,当一个值不在其所允许的范围或者集合中ReferenceError
:引用错误,当一个不存在的变量被引用时发生的错误SyntaxError
:语法错误,Javascript 引擎发现了不符合语法规范的 tokens 或 token 顺序TypeError
:类型错误,表示值的类型非预期类型时发生的错误URIError
:以一种错误的方式使用全局 URI 处理函数而产生的错误InternalError
:表示出现在 JavaScript 引擎内部的错误(遇不到)生成字节码
解释器 lgniton 根据 AST 生成字节码,并解释执行字节码。
字节码就是介于 AST 和机器码之间的一种代码。但是与特定类型的机器码无关,字节码需要通过解释器将其转换为机器码后才能执行。
执行代码
lgnition
会逐条解释执行。HotSpot
(一段代码被重复执行多次,称为热热点代码),后台编辑器Turbofan
会把热点代码的字节码编译为更高效的机器码(同样内存占用更多)即时编译(JIT):字节码配合解释器和编译器
JavaScript 性能优化
四、浏览器页面循环模块
14. 消息队列与事件循环
线程模型的历史
单线程处理任务(第一版)
运行中处理新的任务-引用事件循环机制(第二版)
处理其他线程发送的任务-引入消息队列(第三版)
消息队列是一种数据结构,可以存放要执行的任务。它符合队列“先进先出”的特点,也就是说要添加任务的话,添加到队列的尾部;要取出任务的话,从队列头部去取。
改造三个步骤:
处理其他进程发送的任务-IPC(第四版,也是目前使用的版本)
渲染进程专门有一个 IO 线程用来接收其他进程传进来的消息,接收到消息之后,会将这些消息组装成任务发送给渲染主线程。其他步骤同第三版。
页面使用单线程的缺点
如何处理高优先级的任务
权衡效率和实时性,引用微任务
实现
dom
(高优先级任务) 等放入微任务中如何解决单个任务执行时长过久的问题
回调函数
15. WebAPI 实现
1. 浏览器怎么实现
setTimeout
(延迟队列)setTimeout
(延迟队列):先执行消息队列,后执行延迟队列。(本质是
hashMap
,会检查到期任务,到期了就会执行,所有到期任务都执行结束,才会进入下一轮循环。clearTimeout
取消时,直接通过 id 查找,在hashMap
中删除对应的任务)注意事项
定时器不一定会按时执行(当前任务执行时间过长)
嵌套调用,系统设置最短时间间隔为 4 毫秒
未激活的页面,定时器最小时间间隔 1000 毫秒
延时执行时间有最大值(2147483647 ),大于会立即执行
定时器中 this 指向 全局环境,不是定义时所在的对象
如何绑定 this
2. XMLHttpRequest 是怎么实现的
回调函数 VS 系统调用栈
XMLHttpRequest 运行机制
调用
xhr.send
来发起网络请求XMLHttpRequest 坑
16. 宏任务和微任务
宏任务
宏任务包括:
微任务
微任务就是一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前。
异步回调:
微任务包括:
MutationObserver
监控 DOM 节点Promise.resolve()
或者Promise.reject()
,也会产生微任务。检查点:执行微任务的时间
当前宏任务中的 JavaScript 快执行完成时,也就在 JavaScript 引擎准备退出全局执行上下文并清空调用栈的时候,JavaScript 引擎会检查全局执行上下文中的微任务队列,然后按照顺序执行队列中的微任务。
在 JavaScript 脚本的后续执行过程中,分别通过 Promise 和 removeChild 创建了两个微任务,并被添加到微任务列表中。接着 JavaScript 执行结束,准备退出全局执行上下文,这时候就到了检查点了,JavaScript 引擎会检查微任务列表,发现微任务列表中有微任务,那么接下来,依次执行这两个微任务。等微任务队列清空之后,就退出全局执行上下文。
分析得知:
监听 DOM 变化方法演变
原因:Web 应用需要监视 DOM 变化并及时地做出响应。
版本:
早期版本:Mutation Event
Mutation Event 采用了观察者的设计模式,当 DOM 有变动时就会立刻触发相应的事件,这种方式属于同步回调。有性能问题,已经删除
当前版本:MutationObserver
MutationObserver 将响应函数改成异步调用,可以不用在每次 DOM 变化都触发异步调用,而是等多次 DOM 变化后,一次触发异步调用,并且还会使用一个数据结构来记录这期间所有的 DOM 变化。
MutationObserver 实现:
MutationObserver 采用了“异步 + 微任务”的策略。
17. Promise
异步编程的问题:代码逻辑不连续、地狱回调
封装异步代码
Promise 如何解决地狱回调
创建新的 Promise 对象,并 return 到外层
Promise 与微任务联系
Promise 之所以要使用微任务是由 Promise 回调函数延迟绑定技术导致的。在调用 onResolve 时,then 方法还没有执行
18.async/await
ES7 引入了 async/await,这是 JavaScript 异步编程的一个重大改进,提供了在不阻塞主线程的情况下使用同步代码实现异步访问资源的能力,并且使得代码逻辑更加清晰。
1.生成器和协程
生成器函数是一个带星号函数,而且是可以暂停执行和恢复执行的。
生成器函数使用方式:
在生成器函数内部执行一段代码,如果遇到 yield 关键字,那么 JavaScript 引擎将返回关键字后面的内容给外部,并暂停该函数的执行。
外部函数可以通过 next 方法恢复函数的执行。
协程是一种比线程更加轻量级的存在。协程是跑在线程上的任务,一个线程上可以存在多个协程,但是线程上同时只能执行一个协程。比如当前执行的是 A 协程,要启动 B 协程,那么 A 协程就需要将主线程的控制权交给 B 协程,这就体现在 A 协程暂停执行,B 协程恢复执行;同样,也可以从 B 协程中启动 A 协程。通常,如果从 A 协程启动 B 协程,我们就把 A 协程称为 B 协程的父协程。
从图中可以看出来协程的四点规则:
父协程与 gen 协程是交互执行,通过 yield 和 gen.next 配合完成,JavaScript 引擎会保存调用栈信息。
2. async/await
async/await 技术背后的秘密就是 Promise 和生成器应用,往低层说就是微任务和协程应用。
async
根据 MDN 定义,async 是一个通过异步执行并隐式返回 Promise 作为结果的函数。
await
返回结果依次为 0->1->3->100->2
打印 0
执行 foo,因为 foo 是 async 标记过的,引擎会保存当前调用栈信息,将主线程控制权交给 foo 协程,打印 1
执行 await 100,默认创建 Promise 对象
引擎暂停当前 foo 协程执行,将主线程控制权交给父协程,同时将 promise_对象返回父协程
父协程调用 promise_.then 监控 promise 状态的改变,打印 3
父协程执行结束,进入微任务检查点,执行微任务队列,其中微任务队列中有 resolve(100)任务等待执行,执行 resolve 时,会触发 promise_.then 中回调函数
五、 浏览器中的页面
17. chrome 开发者工具
待整理。。
18. DOM 树
HTML 解析器(随着 HTML 文档加载边加载边解析)
网络进程和渲染进程之间会建立一个共享数据的管道
字节流转换 DOM 三个阶段
JavaScript 会阻塞 DOM,CSS 会阻塞 JavaScript 的执行,不阻塞 JavaScript 的加载
19. 渲染流水线 CSS
CSSOM
缩短白屏时间
<link media='' />
20. 分层与合成机制
21. 优化页面性能
加载阶段
阻塞网页首次渲染的资源称为关键资源
优化原则:减少关键资源个数,降低关键资源大小,降低关键资源 RTT 次数
资源个数
大小
RTT
交互阶段(优化帧率)
减少 JavaScript 脚本执行时间
避免强制同步布局:是指 JavaScript 强制将计算样式和布局操作提前到当前的任务中
避免布局抖动(多次同步布局)
合理利用 CSS 合成动画(合成线程)
避免频繁的垃圾回收(优化存储结构,避免小颗粒对象产生)
22. 虚拟 DOM 和实际 DOM
双缓存:虚拟 DOM 是双缓存的实现
MVC 模式
27. 渐进式网页应用(PWA)
28. webcomponent(对内高内聚,对外低耦合)
全局属性会影响组件化,相同的样式会被覆盖
WebComponent 组件化开发
三元素:
步骤:
浏览器如何实现影子 DOM?
DOMAPI 无法查询影子 DOM
过 DOM 接口去查找元素时,渲染引擎会去判断 geek-bang 属性下面的 shadow-root 元素是否是影子 DOM,如果是影子 DOM,那么就直接跳过 shadow-root 元素的查询操作。
CSS 样式
当生成布局树的时候,渲染引擎也会判断 geek-bang 属性下面的 shadow-root 元素是否是影子 DOM,如果是,那么在影子 DOM 内部元素的节点选择 CSS 样式的时候,会直接使用影子 DOM 内部的 CSS 属性。
六、浏览器中的网络:HTTP
1. 超文本传输协议 HTTP0.9
2. 被浏览器推动的 HTTP1.0
3. 缝缝补补的 HTTP1.1
改进持久连接
HTTP/1.1 中增加了持久连接的方法,它的特点是在一个 TCP 连接上可以传输多个 HTTP 请求,只要浏览器或者服务器没有明确断开连接,那么该 TCP 连接会一直保持。
持久连接可以有效减少 TCP 建立连接和断开连接的次数,HTTP1.1 中默认开启
不成熟的 HTTP 管线化
队头阻塞问题
提供虚拟主机的支持 Host
增加了 Host 字段
对动态生成的内容提供了完美支持
引入(Chunk transfer 机制)
客户端 Cookie 安全机制
4. HTTP2
HTTP1.1 优化
HTTP1.1 问题
带宽的利用率不理想
多路复用的实现:引入二进制分帧层
请求和接收过程:
从上面的流程可以看出,通过引入二进制分帧层,就实现了 HTTP 的多路复用技术。
HTTP2 其他特性
可以设置请求优先级
服务器推送,请求 HTML 文件时同时拿到 HTML,CSS,JavaScript 文件
头部压缩
5. HTTP3(未来)
TCP 队头阻塞:由于单个数据包的丢失而造成的阻塞,丢包率达到 2%时,HTTP1.1 效率更高
TCP 建立连接(握手)的延时(RTT)
我们把从浏览器发送一个数据包到服务器,再从服务器返回数据包到浏览器的整个往返时间称为 RTT。
RTT 是反映网络性能的一个重要指标。
TCP 协议及中间设备僵化,操作系统也是导致 TCP 协议僵化的另一个原因
QUIC 协议
QUIC:基于 UDP 实现类似 TCP 多路复用数据流(解决队头阻塞),传输可靠性等功能
QUIC 的实现:(TCP+HTTP2 多路复用+TLS)
实现了类似 TCP 的流量控制. 传输可靠性的功能
集成 TLS 加密功能
实现了 HTTP2 多路复用
QUIC 实现了在同一物理连接上有多个独立逻辑数据流,实现了数据流的单独传输,解决了队头阻塞
实现快速握手,因为是基于 UDP,0-RTT 或者 1-RTT 建立连接
七、浏览器网络安全
29. 同源策略(Same-origin)
同源
如果两个 URL 的协议、域名和端口都相同,我们就称这两个 URL 同源。
同源策略主要表现在 DOM,Web 数据和网络三个方面
安全和便利性的权衡
页面中可以嵌入第三方资源
为了解决 XSS 攻击,浏览器中引入了内容安全策略,称为 CSP。CSP 的核心思想是让服务器决定浏览器能够加载哪些资源,让服务器决定浏览器是否能够执行内联 JavaScript 代码。
跨域资源共享和跨文档消息机制
跨域资源共享(CORS):可以跨域访问
跨文档消息机制:两个不同源 DOM 之间通信,可以通过
window.postMessage
通信。30.跨站脚本攻击 XSS(跨站脚本,恶意注入脚本)
做的小坏事
恶意脚本如何注入的
1、存储型 XSS 攻击
用户想网站请求包含恶意 JavaScript 脚本的页面
当用户浏览该页面时,恶意脚本就会将用户的 Cookie 信息上传服务器
2、反射型 XSS 攻击
3、基于 DOM 的 XSS 攻击:修改 HTML 页面内容
阻止的策略
只能在 HTTP 请求中,不能在 JavaScript 中读取 cookie,不能通过 document.cookie 读取
31. CSRF 攻击(跨站请求伪造)
CSRF 攻击就是黑客利用用户的登录状态,并通过第三方的站点来做坏事
CSRF 攻击不需要将恶意代码注入用户页面,仅仅是利用服务器的漏洞和用户登录状态来实施攻击
如何阻止 CSRF
1、利用 cookie 的 samesite 属性:Cookie 是浏览器和服务器之间维护登录状态关键数据
set-cookie:; expires=Tue, 19-Nov-2019 06:36:21 GMT; path=/; domain=.google.com; SameSite=none
2、验证请求的来源站点
3、CSRF Token:服务器生成字符串植入到页面中,并且每次请求需要传入字符串,请求没有 token 会拒绝
浏览器系统安全
安全沙箱
影响
HTTPS
HTTPS 协议栈中引入安全层(SSL/TLS)
对发起 HTTP 请求数据进行加密操作和对接到 HTTP 内容进行解密操作
加密
非对称加密算法有 A、B 两把密钥,如果你用 A 密钥来加密,那么只能使用 B 密钥来解密;反过来,如果你要 B 密钥来加密,那么只能用 A 密钥来解密。
公钥是每个人都能获取到的,而私钥只有服务器才能知道,不对任何人公开。
混合加密(数据用对称加密,密钥用非对称加密)
数字证书(保证网站是安全的,证明网站是要访问的安全网站)
The text was updated successfully, but these errors were encountered: