最近在极客时间学习
<浏览器工作原理>
一课的一些笔记
开篇词的学习
浏览器进化的三个阶段
- 应用程序化
- web应用移动化
- web操作系统化
了解浏览器工作原理的两大优势
- 准确评估项目的可行性(需多语言基础,及后端知识)
- 更高纬度审视页面,如:
- 用户请求首屏加载慢
- ssr技术 , 如Airbnb主
- 缓存(cache-control) , 优化接口请求次数 , 打包文件体积优化(gzip)
- 操作dom某个功能元素,如button反应迟钝
- 优化该元素功能函数的内部逻辑(减少console.log()、优化冗余逻辑)
- 审视当前页面的性能消耗 , 如减少监听器 或 通过eventhub实现发布订阅等
- web中动画没有60帧
- ??? 没遇到过做动画的情况
- 2020-08-15 补充 : 通过设置 CSS
will-change
属性,使元素单独分层,优化动画。
- 用户请求首屏加载慢
首屏加载问题
- DNS, HTTP解析
- DOM 解析
- CSS 阻塞
- JS 阻塞
以上任意一步产生问题,均将造成加载延迟
宏观视角下的浏览器的学习
第一课 (打开chorme有4个进程)
打开浏览器,如
chorme
会有4个进程
进程和线程的四大特点 :
- 进程 中任意线程出错 , 导致整个 进程 崩溃
线程
之间共享进程
中的数据- 当一个进程关闭之后,操作系统会回收进程所占用的内存
- 进程之间的内容相互隔离
单进程浏览器缺点:
- 不稳定
- 一个插件会引起整个浏览器崩溃
- 复杂的JavaScript代码也会引起浏览器崩溃
- 不流畅
- 如代码中有无限循环的判断条件,单进程会独占所有内存来执行
- 不安全
- 插件可以通过 c/c++来编写,即 可以访问操作系统人以资源
- 页面脚本则可通过浏览器漏洞获取系统权限(盗号)
- 不稳定
多线程浏览器解决以上问题
- 不稳定
- 进程相互隔离,不会导致浏览器崩溃
- 不流畅
- 即使 js文件 渲染阻塞也仅仅影响
当前
页面,同时内存泄露也只需关闭当前页面便能垃圾回收
- 即使 js文件 渲染阻塞也仅仅影响
- 不安全
- 多进程有额外的安全沙箱, chorme 吧插件进程所在安全沙箱内(safe sandbox)
- 不稳定
最新浏览器具备5个进程
- 浏览器主进程
- 界面显示,用户交互,子进程管理,存储等
- GPU进程
- 实现 3D CSS 效果
- 网络进程
- 负责网络资源加载
- 渲染进程
- HTML,CSS,JS文件的渲染
- 插件进程
- 运行插件
打开浏览器至少有以上4个进程(除插件)
- 浏览器主进程
第二课 (TCP)
FP(first paint):指从页面加载到首次开始绘制的时长.
首先要确定一个观点, 互联网中文件传输是通过数据包
来传输的.
一个数据包从
主机A
到主机B
的传输过程:主机A
的上层传递数据包给主机A
网络层主机A
的网络层添加IP头并组成新的IP数据包
传到底层主机A
底层通过物理传输(链路层)给目标主机B
主机B
网络层接受并开始解析传递过来的数据包(拆开IP头)并把拆下IP头的数据包传递给主机B
的上层主机B
的上层接受网络层传过来的数据包
UDP协议: 用户数据包协议(User Datagram Protocol)
- UDP不保证数据可靠性,传输速率快
- 此时需在基于添加
IP头
的 网络层之上的传输层
添加UDP协议 - 一个数据包从
主机A
到主机B
的传输过程(包含UDP协议):主机A
的上层传递数据包给主机A
传输层主机A
的传输层添加UDP头
并组成新的数据包
传到 网络层主机A
的网络层添加IP头并组成新的IP数据包
传到底层主机A
底层通过物理传输(链路层)给目标主机B
主机B
网络层 接受并开始解析 传递过来的数据包(拆开IP头)并把拆下IP头的数据包传递给主机B
的 传输层主机B
网络层解析完毕后的数据包传递给主机B
的 传输层,并开始解析(拆开UDP头),并把拆下后的数据包传递给主机B
的上层主机B
收到来自主机B
传输层的数据包
- UDP协议的缺点:
- 数据包在传输过程中容易丢失
- 大文件拆分来传递,UDP并不能组合这些数据包
TCP:把数据完整地送达应用程序
- TCP协议解决UDP协议的两个问题:
- 对于数据包丢失的情况,TCP 提供重传机制
- TCP 引入了数据包排序机制,用来保证把乱序的数据包组合成一个完整的文件
- 数据传输流程同UDP协议,唯一不同的是协议由UDP头变更为TCP头
- 完整的TCP链接又包含3个步骤
三次握手,建立链接
(以下以客户C(client)端和服务器S(sever)来举例)- C->S 传递一个数据包,并对S说:我能建立链接了吗 (第一次)
- S->C S收到数据包 , 确认过后 , 对C说 : 可以建立链接了 (第二次)
- C->S C收到S的数据包 , 确认可以连接了 , 此时三次握手完毕 (第三次)
- 传输阶段 开始进行数据传输
四次挥手,断开链接
- S->C 传输完所有数据后,对C说,我数据传输完毕了。
- C->S 接受后对 S 说,哦,我知道了。
- C->S 再次发送消息,我也传输完了。
- S->C 接受后,哦,我知道了,此时完成四次挥手。
- TCP协议解决UDP协议的两个问题:
总结
- 数据传输通过数据包链接,数据包在传输过程中易丢失和出错
- IP 负责把数据包送达目的主机
- UDP 负责把数据包送达具体应用
- TCP 保证了数据完整地传输,它的连接可分为三个阶段:建立连接、传输数据和断开连接
第三课 (构建HTTP请求)
主要是关于浏览器构建HTTP请求的过程
HTTP是 建立在 TCP 连接基础 之上的, 一种允许浏览器向服务器获取资源的协议,是 Web 的基础
浏览器端发起 HTTP 请求流程:
- 构建请求:一般是
GET / index.html HTTP 1.1
- 查找缓存(解决两个问题)
- 缓解服务器压力,提升性能
- 对网站来说,缓存是实现快速资源加载的重要部分
- 准备IP地址和端口(DNS查询)
- 等待TCP队列(TCP队列不超过6个)
- 建立TCP连接(三次握手建立连接,传输数据,四次挥手断开连接)
- 发送HTTP请求
- 构建请求:一般是
服务器端处理 HTTP 请求流程 :
- 返回请求
- 带上HTTP报文 和 HTTP 实体 返回 响应
- 响应包括响应行 响应头 响应体
- 通过状态码来查询响应的状态
- 断开连接(如果加入Connection:Keep-Alive,TCP便不会断开连接)
- 重定向
- 返回请求
为什么很多站点第二次打开速度会很快?
- 因为缓存的机制
- 浏览器构建HTTP请求,从服务器得到响应(添加了cache-control,此时便有缓存)
- 第二次进入相同站点,服务器收到HTTP请求会先查询是否有cache-control,如果未过期,则直接跳过该部分资源的请求
- 如果缓存过期,则请求该缓存部分最新数据,再次进行缓存
登录状态是如何保持的?
- cookie 通过 服务器 响应 添加
Set-Cookie
进行设置的 - 用户登录后 发送请求 携带上
Cookie
- 服务器收到 请求后对比 cookie 检验是否过期,未过期则保持登录状态
- cookie 通过 服务器 响应 添加
第四课(为4-6的总结 页面从导航到展示的过程)
只解决一个问题,从输入URL到页面展示,这中间发生了什么?
简易流程
- 浏览器进程收到用户输入的URL请求,浏览器进程将URL转发给网络进程
- 网络进程发起真正的URL请求
- 网络进程收到响应头,开始解析响应头数据,并将数据转发给浏览器进程
- 浏览器进程收到网络进程响应头数据后,发送
提交导航
消息给渲染进程 - 渲染进程接到
提交导航
消息后 ,开始准备接受HTML数据,接受方式是与网络进程建立连接通道
- 最后渲染进程向浏览器进程
确认提交
- 浏览器进程收到渲染进程的
提交文档
消息后,开始移除之前的旧文档,更新浏览器进程中的页面状态 - 小结:用户发出的URL请求到页面开始解析的过程叫导航
详细流程
- 用户输入(判断输入关键字还是URL)
- 如果是搜索内容,地址栏会使用浏览器默认的搜索引擎,来合成新的带搜索关键字的 URL。
- 如果是链接,浏览器加上HTTP协议,并传递给网络层
- URL请求过程
- 查找缓存
- 有缓存: 进入网络进程解析响应流程
- 无缓存:
- 进行DNS解析, 获取IP地址和端口
- 利用IP地址和服务器建立TCP连接
- 构建请求头
- 发送请求头
- 进入网络进程解析响应流程
- 查找缓存
- 网络进程解析响应过程:
- 1开头, 表明连接建立 , 还在处理中
- 2开头, 表明响应成功 , 走到第 4 步
- 3开头, 表示重定向 , 重新由步骤 2 开始 , 向重定向的地址发送请求
- 4开头, 表示客户端错误 , 返回错误信息
- 5开头, 表示服务器错误 , 返回错误信息
- 渲染进程接收
提交导航
- 将HTML文件转换成浏览器看得懂的语言, 生成DOM树
- 将CSS文件转换成浏览器看得懂的语言
stylesheet
, 并计算DOM树中各个节点的CSS属性 - 渲染进程根据DOM树中各个节点 , 生成对应的
布局树
- 对布局树进行分层 , 并生成
分层树
z-index - 为每个图层生成绘制列表 , 并提交给
合成进程
合成进程
将图层分成土块,并在光栅化线程池中将图块转换成位图- 合成线程发送绘制图块命令
DrawQuad
给浏览器进程 - 浏览器进程根据
DrawQuad
消息生成页面, 并显示到显示器上
- 用户输入(判断输入关键字还是URL)
以上就是浏览器输入URL到页面展示过程的完整过程
总结
- 优化首屏加载速度可以从4个方面入手
- JS文件阻塞
- CSS文件阻塞
- HTML文件阻塞
- DNS, HTTP解析
- 浏览器渲染进程中,冗余的css会造成浏览器的额外开销
- 重排: 更新元素几何属性, 例如: 高度, 宽度 , 开销最大
- 重绘: 更新元素的绘制属性, 例如: 背景色 , 开销其次
- 直接合成: 如transition, 开销最小
- 减少重排,重绘的解决办法
- 用class替代style
- 避免table布局
- 批量操作DOM,如框架
- 禁用浏览器的Debounce resize 事件
- 对DOM属性进行读写分离
- will-change(上下层叠属性):transform 优化
- 优化首屏加载速度可以从4个方面入手
浏览器中JavaScript执行机制
第一课 (变量提升)
需要搞懂js 的执行上下文
以
var a = 1
为例, 到输出结果的过程- 对语句进行词法分析, 结果为
var , a , = , 2
- 进行语法分析, 形成抽象语法树, 其根(root)为 var 的顶级节点, 叶子节点为 a 和 2
- 代码生成, 创建一个 a 的变量(LHS), 并将 2 赋值给 a (RHS), 过程为 ,
var a
和a = 2
- 对语句进行词法分析, 结果为
变量提升
- 上述过程清晰阐述了语句
var a = 1
的过程, 在期间,使用var
来声明变量, 会使得var a
语句提升到 全局执行上下文的最前面, 再对其进行赋值操作. - 这就是为什么 console.log(a) 在前 var a = 2 在后 ,却能打出 a 的值为2的分析过程
- 如果在此过程中声明函数
function a(){}
, a 会被 function 覆盖.
- 上述过程清晰阐述了语句
第二课 (调用栈)
总得来说, 执行函数, 是先将函数压入调用栈, 执行完毕后, 再弹出调用栈的操作
栈的特点是后进先出, js的调用栈称为执行上下文栈
1 | var a = 2 |
- 考虑上述代码 , 其执行顺序为 :
- 从全局执行上下文中,取出 add 函数代码。
- 对 add 函数的这段代码进行编译,并创建该函数的执行上下文和可执行代码。
- 执行代码,输出结果。
- 抽象对js函数执行的理解 , 大致步骤如下 :
- 创建全局上下文,并将其压入栈底。
- 第二步是调用 add 函数。
- 如果 add 函数内部还有函数 , 将 add 内部执行函数 压入 调用栈
- 当
add 内部函数
执行完毕 , 返回结果 , 并弹出栈 , 开始执行 add 函数 - 当 add 函数 执行完毕 , 返回值
第三课 (作用域和闭包)
作用域是指 变量与函数的可访问范围
ES6之前 , 作用域分 全局作用域 和 函数作用域
ES6之后 , 出现 块级作用域
- 全局作用域 : 对象在代码中的任何地方都能访问,其生命周期伴随着页面的生命周期。用
var
声明的变量, 存储在变量环境
中 - 函数作用域 : 函数内部定义的变量或者函数,并且定义的变量或者函数只能在函数内部被访问。函数执行结束之后,函数内部定义的变量会被销毁。
- 闭包 :
- 块级作用域 : 比如函数、判断语句、循环语句,甚至单独的一个{}都可以被看作是一个块级作用域 , 块级作用域内的变量 仅在块级作用域内有效。
- 在块级作用域内使用
let
和const
声明变量 , 会形成 暂死区 , 此时得不到变量的值,但是词法环境
中却存在该变量
- 在块级作用域内使用
- 作用域链 : 当前执行函数, 如果引用了本身没有的变量 , 会从本层的外层寻找该变量 , 考虑函数执行上下文 , 并分析 outer 为 上一个函数的作用域 , 还是 全局作用域.
第四课 (this)
关键字
this
不会凭空出现 , 分析其 执行上下文 , 就能得出this
的指向
1 | let obj = { |
- 绑定
this
的 三种形式 :- 显示绑定 :
- obj.getName.call(obj)
- obj.getName.apply(obj)
- obj.getName.bind(this)()
- .bind()操作只返回一个函数,需要自己手动调用
- .call()和.apply()不需要手动调用,会自动调用,仅仅是传参不同
- 隐式绑定 :
- x() === window.x.call(window) , 结果为 window 和 空
- new 操作的 this 指向 new 出来的那个对象 , new 的操作如下
- 新建一个临时对象 let newTemp = {}
- 调用 createObj.call(newTemp)
- 执行 createObj 函数
- 返回这个临时对象
- 箭头函数中的 this 与执行上下文中的 this 相同
- 显示绑定 :
V8工作原理
第一课(栈和堆)
JavaScript 的 7 种基本类型分别是
Number,String,Boolean,BigInt,Symbol,Null和Undefined.
JavaScript 的 引用类型只有一种 Object
- js存储方式
- 基本类型 存储于
栈内存
中, 栈内存仅仅只存储引用类型的地址
- 引用类型 存储于
堆内存
中, 堆中的数据是通过引用和变量关联起来的
- 基本类型 存储于
第二课(垃圾回收)
有些数据被使用之后,可能就不再需要了,我们把这种数据称为垃圾数据。
如果这些垃圾数据一直保存在内存中,那么内存会越用越多,
所以我们需要对这些垃圾数据进行回收,以释放有限的内存空间。
- 不同语言的垃圾回收政策
- 手动回收
- 手动使变量的值为null
- 自动回收
- JS执行过后的变量垃圾也分为两种
- 新生代的垃圾
- 副垃圾回收器,主要负责新生代的垃圾回收。
- 老生代的垃圾
- 主垃圾回收器,主要负责老生代的垃圾回收。
- 新生代的垃圾
- JS执行过后的变量垃圾也分为两种
- 手动回收
- 垃圾不回收造成的问题
- 内存泄漏
- 直观的感受就是页面越来越卡
- 避免以上的原因
- 尽量少使用闭包, 闭包会使函数的引用变量一直存放在内存中, 长时间占用内存
- 确定不使用的临时变量时使其值为 null
第三课(V8如何执行一段JS代码)
需要理解的几个重要概念
编译器(Compiler)、解释器(Interpreter)、抽象语法树(AST)、
字节码(Bytecode)、即时编译器(JIT)
- 在编译器语言编译过程中
- 编译器对源代码进行词法分析和语法分析, 生成抽象语法树
- 优化代码
- 最后生成处理器能理解的代码
- 如果编译成功,将会生成一个可执行的文件。
- 如果编译过程发生了语法或者其他的错误,那么编译器就会抛出异常,最后的二进制文件也不会生成成功。
- 在解释型语言的解释过程中
- 同样解释器也会对源代码进行词法分析、语法分析,并生成抽象语法树(AST)
- 不过它会再基于抽象语法树生成字节码
- 最后再根据字节码来执行程序、输出结果。
- 抽象语法树
- 是源代码语法结构的一种抽象表示
- 字节码
- 字节码就是介于 AST 和机器码之间的一种代码。但是与特定类型的机器码无关,字节码需要通过解释器将其转换为机器码后才能执行。
- 即时编译器
- 字节码配合解释器和编译器是最近一段时间很火的技术,比如 Java 和 Python 的虚拟机也都是基于这种技术实现的,我们把这种技术称为即时编译(JIT)
- 具体到 V8,就是指解释器 Ignition 在解释执行字节码的同时,收集代码信息,当它发现某一部分代码变热了之后,TurboFan 编译器便闪亮登场,把热点的字节码转换为机器码,并把转换后的机器码保存起来,以备下次使用。
- JS在执行一段代码的过程
- 生成抽象语法树(AST)和执行上下文
- 第一阶段是
分词
, 如 : var a = 1, 将被分为var
,a
,=
,1
- 第二阶段是
解析
, 又称语法分析
, 其作用是将上一步生成的 token 数据,根据语法规则转为 AST。- 如果源码符合语法规则,这一步就会顺利完成。
- 但如果源码存在语法错误,这一步就会终止,并抛出一个“语法错误”。
- 生成字节码
- 执行代码
- 第一阶段是
- 生成抽象语法树(AST)和执行上下文
- 综上所述, 包括之前的知识, 对JS性能优化的总结
- 提升单次脚本的执行速度,避免 JavaScript 的长任务霸占主线程,这样可以使得页面快速响应交互
- 避免大的内联脚本,因为在解析 HTML 的过程中,解析和编译也会占用主线程
- 减少 JavaScript 文件的容量,因为更小的文件会提升下载速度,并且占用更低的内存
- 为什么V8代码执行时间越久,执行效率越高?
- 即时编译(JIT)技术的存在
- 解释器执行字节码的过程中,如果发现有热点代码(HotSpot),
- 那么后台的编译器 TurboFan 就会把该段热点的字节码编译为高效的机器码,
- 然后当再次执行这段被优化的代码时,只需要执行编译后的机器码就可以了,
- 这样就省去了省去了字节码“翻译”为机器码的过程大大提升了代码的执行效率。
浏览器中页面循环系统
消息队列和事件循环
JS处理代码时的顺序
所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
主线程之外,还存在一个”任务队列”(task queue)。
只要异步任务有了运行结果,就在”任务队列”之中放置一个事件。
一但”执行栈”中的所有同步任务执行完毕,系统就会读取”任务队列”,看看里面有哪些事件。
那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
主线程不断重复上面的第三步。
只要主线程空了,就会去读取”任务队列”,这就是JavaScript的运行机制。
这个过程会不断重复,这种机制就被称为事件循环(event loop)机制。
消息队列是一种数据结构,可以存放要执行的任务。它符合队列“先进先出”的特点。
也就是说要添加任务的话,添加到队列的尾部;要取出任务的话,从队列头部去取。
- 总结
- 如果有一些确定好的任务,可以使用一个单线程来按照顺序处理这些任务,这是第一版线程模型
- 要在线程执行过程中接收并处理新的任务,就需要引入循环语句和事件系统,这是第二版线程模型。
- 如果要接收其他线程发送过来的任务,就需要引入消息队列,这是第三版线程模型。
- 如果其他进程想要发送任务给页面主线程,那么先通过 IPC 把任务发送给渲染进程的 IO 线程,IO 线程再把任务发送给页面主线程。
- 消息队列机制并不是太灵活,为了适应效率和实时性,引入了微任务。
Web Api
setTimeout 和 XMLHttpRequest (宏任务)
- setTimeout
- 如果当前任务执行时间过久,会影响定时器任务的执行。
- 如果 setTimeout 存在嵌套调用,那么系统会设置最短时间间隔为 4 毫秒。
- 未激活的页面,setTimeout 执行最小间隔是 1000 毫秒。
- 延时执行时间有最大值。
- 使用 setTimeout 设置的回调函数中的 this 不符合直觉。
- XMLHttpRequest(AJAX)
- 创建 XMLHttpRequest 对象。
- 为 xhr 对象注册回调函数。
- 配置基础的请求信息。
- 发起请求。
1 | setTimeout |
微任务和宏任务
微任务可以在实时性和效率之间做一个有效的权衡。
- 宏任务
- 渲染事件(如解析 DOM、计算布局、绘制);
- 用户交互事件(如鼠标点击、滚动页面、放大缩小等);
- JavaScript 脚本执行事件;
- 网络请求完成、文件读写完成事件。
- 微任务
- 把异步回调函数封装成一个宏任务,添加到消息队列尾部,当循环系统执行到该任务的时候执行回调函数。
- 执行时机是在主函数执行结束之后、当前宏任务结束之前执行回调函数,这通常都是以微任务形式体现的。
- 结论
- 微任务和宏任务是绑定的,每个宏任务在执行时,会创建自己的微任务队列。
- 微任务的执行时长会影响到当前宏任务的时长。比如一个宏任务在执行过程中,产生了 100 个微任务,执行每个微任务的时间是 10 毫秒,那么执行这 100 个微任务的时间就是 1000 毫秒,也可以说这 100 个微任务让宏任务的执行时间延长了 1000 毫秒。所以你在写代码的时候一定要注意控制微任务的执行时长。
- 在一个宏任务中,分别创建一个用于回调的宏任务和微任务,无论什么情况下,微任务都早于宏任务执行。
- 每一次事件循环顺序都是宏->微->宏,每次清空当前消息队列时会检查微任务队列中是否已清空。
Promise、Async/Await
Promise 解决回调地狱的问题
Async/Await 用同步的写法来编写异步代码
- Promise
.then()
的写法消灭嵌套调用.catch()
合并多个错误处理.race()
多个请求只取最快.all()
等待请求全部完成.finally()
无论resolve
或reject
最后都会走到这个分支
- Async/Await
- 该方法是 generator 生成器,调用 generator.next().value 的语法糖
- 生成器函数是一个带星号函数,而且是可以暂停执行和恢复执行的。
- 在生成器函数内部执行一段代码,如果遇到 yield 关键字,那么 JavaScript 引擎将返回关键字后面的内容给外部,并暂停该函数的执行。
- 外部函数可以通过 next 方法恢复函数的执行。
- Async 是一个通过异步执行并隐式返回 Promise 作为结果的函数。
- 该方法是 generator 生成器,调用 generator.next().value 的语法糖
浏览器中的页面
Chrome开发工具
Chrome 开发者工具(简称 DevTools)是一组网页制作和调试的工具,
内嵌于 Google Chrome 浏览器中。
- Elements面板
- 可查看DOM结果、编辑CSS样式,用于测试页面布局和设计页面。
- Console面板
- 可以看成是JavaScript Shell,能执行JavaScript脚本。通过Console在页面中与JavaScript对象交互。
- Network面板
- 展示页面中所有请求内容列表,能查看每项请求的请求行、请求头、请求体、时间线以及网络请求瀑布图等信息。
- 可根据网络请求来观察HTTP 1.1 下 当前浏览器最多支持 6 个 TCP 连接。
- 把站点升级到 HTTP 2 , 可突破上述问题限制,因为 HTTP 2 的多路由复用机制。
- Source面板
- 查看Web应用加载的所有文件
- 编辑CSS和JavaScript文件内容
- 将打乱的CSS文件或者JavaScript文件格式化
- 支持JavaScript的调试功能
- 设置工作区,将更改的文件保存到本地文件夹中
- Performance面板
- 记录和查看Web应用生命周期内的各种事件,并用来分析在执行过程中一些影响性能的要点。
- Memory面板
- 用来查看运行过程中JavaScript占用堆内存情况,追踪是否存在内存泄露的情况等
- Application面板
- 查看Web应用的数据存储情况
- PWA的基础数据
- IndexedDB
- Web SQL
- 本地和会话存储
- Cookie
- 应用程序缓存
- 图像
- 字体和样式表
- 查看Web应用的数据存储情况
- Security面板
- 显示当前页面的一些基础安全信息
JavaScript是如何影响DOM树创建的
从网络传给渲染引擎的 HTML 文件字节流是无法直接被渲染引擎理解的,
所以要将其转化为渲染引擎能够理解的内部结构,这个结构就是 DOM。
DOM 提供了对 HTML 文档结构化的表述。
- 在渲染引擎中,DOM 有三个层面的作用。
- 从页面的视角来看,DOM 是生成页面的基础数据结构。
- 从 JavaScript 脚本视角来看,DOM 提供给 JavaScript 脚本操作的接口,通过这套接口,JavaScript 可以对 DOM 结构进行访问,从而改变文档的结构、样式和内容。
- 从安全视角来看,DOM 是一道安全防护线,一些不安全的内容在 DOM 解析阶段就被拒之门外了。
- DOM 树如何生成
- 首先确认一个观念,HTML 解析器并不是等整个文档加载完成之后再解析的,而是网络进程加载了多少数据,HTML 解析器便解析多少数据。
- 网络进程接受到响应头之后,会根据响应头中的
content-type
字段判断文件类型,如果值为text/html
,浏览器会判断这是一个HTML类型,然后为该请求选择或创建一个渲染进程。 - 渲染进程准备好之后,网络进程和渲染进程之间会建立一个共享数据的管道,网络进程接收到数据后就往这个管道里面放,而渲染进程则从管道的另外一端不断地读取数据,解析器就将收到的字节流解析为DOM。
- DOM形成的详细过程
- 第一个阶段,通过分词器将字节流转换为 Token。
- 如果压入到栈中的是 StartTag Token,HTML 解析器会为该 Token 创建一个 DOM 节点,然后将该节点加入到 DOM 树中,它的父节点就是栈中相邻的那个元素生成的节点。
- 如果分词器解析出来是文本 Token,那么会生成一个文本节点,然后将该节点加入到 DOM 树中,文本 Token 是不需要压入到栈中,它的父节点就是当前栈顶 Token 所对应的 DOM 节点。
- 如果分词器解析出来的是 EndTag 标签,比如是 EndTag div,HTML 解析器会查看 Token 栈顶的元素是否是 StarTag div,如果是,就将 StartTag div 从栈中弹出,表示该 div 元素解析完成。
- 至于后续的第二个和第三个阶段是同步进行的,需要将 Token 解析为 DOM 节点,并将 DOM 节点添加到 DOM 树中。
- 第一个阶段,通过分词器将字节流转换为 Token。
- JavaScript 是如何影响 DOM 生成的
- 内联
<script>console.log(1)</script>
会阻塞DOM树的形成。 - 标签内插入下载的JS文件
<script type="text/javascript" src='foo.js'></script>
- 内联
CSS如何影响首次加载时的白屏时间
当渲染进程接收 HTML 文件字节流时,会先开启一个预解析线程,
如果遇到 JavaScript 文件或者 CSS 文件,那么预解析线程会提前下载这些数据。
- CSSOM 的作用
- 提供给 JavaScript 操作样式表的能力。
- 为布局树的合成提供基础的样式信息。
- 影响页面展示的因素
- 主要原因就是渲染流水线影响到了首次页面展示的速度。
- 视觉上经历的3个阶段
- 等请求发出去之后,到提交数据阶段,这时页面展示出来的还是之前页面的内容。
- 提交数据之后渲染进程会创建一个空白页面,我们通常把这段时间称为解析白屏,并等待 CSS 文件和 JavaScript 文件的加载完成,生成 CSSOM 和 DOM,然后合成布局树,最后还要经过一系列的步骤准备首次渲染。
- 等首次渲染完成之后,就开始进入完整页面的生成阶段了,然后页面会一点点被绘制出来。
- 优化策略
- 通过内联 JavaScript、内联 CSS 来移除这两种类型的文件下载,这样获取到 HTML 文件之后就可以直接开始渲染流程了。
- 但并不是所有的场合都适合内联,那么还可以尽量减少文件大小,比如通过 webpack 等工具移除一些不必要的注释,并压缩 JavaScript 文件。
- 还可以将一些不需要在解析 HTML 阶段使用的 JavaScript 标记上 sync 或者 defer。
- 对于大的 CSS 文件,可以通过媒体查询属性,将其拆分为多个不同用途的 CSS 文件,这样只有在特定的场景下才会加载特定的 CSS 文件。
为什么CSS动画比JavaScript动高效?
因为
will-change
属性的存在,
使得浏览器在解析 CSS 文件时会具有该属性的元素提前分层,
后续仅仅只在渲染主进程内的合成线程进行合成,该方法是效率最高的,
JavaScript修改元素样式,可能会引起 重排或重绘 皆是效率最低的。
- 显示器如何显示图像
- 前缓冲区 : 每秒固定读取 60 次前缓冲区中的图像,并将读取的图像显示到显示器上。
- 后缓冲区 : 显卡的职责就是合成新的图像,并将图像保存到后缓冲区中。
- 帧和帧率
- 帧 : 一张图片理解为帧。
- 帧率 : 1s 内更新帧数的频率,如,1s内更新60帧,帧率为60Hz(60FPS)。
- 如何生成一帧图像,通常渲染路径越长,生成图像花费的时间就越多。
- 重排 : 需要重新根据 CSSOM 和 DOM 来计算布局树,这样生成一幅图片时,会让整个渲染流水线的每个阶段都执行一遍,如果布局复杂的话,就很难保证渲染的效率了。
- 重绘 : 没有了重新布局的阶段,操作效率稍微高点,但是依然需要重新计算绘制信息,并触发绘制操作之后的一系列操作。
- 合成 : 操作的路径就显得非常短了,并不需要触发布局和绘制两个阶段,如果采用了 GPU,那么合成的效率会非常高。
- 分层和合成 : 为了提升每帧的渲染效率,Chrome 引入了分层和合成的机制。
- 分层 : 将素材分解为多个图层称为分层,分层体现在生成布局树之后。
- 合成 : 将这些图层合并到一起称为合成,合成操作是在合成线程上完成的,这也就意味着在执行合成操作时,是不会影响到主线程执行的。
- 分块 : 合成线程会将每个图层分割为大小固定的图块,然后优先绘制靠近视口的图块,这样就可以大大加速页面的显示速度。
- 分层和合成 : 为了提升每帧的渲染效率,Chrome 引入了分层和合成的机制。
- 如何利用分层技术优化代码
will-change
属性,通过例子可以观察到,在chrome devtools的 performance面板、layers面板中可以观察到 具有该属性的元素会被提前分层成为单独的一帧图片 从而使得元素不需要重排或重绘,提高了动画合成的效率,但是该操作会占用相对大的内存,根据不同情况下考量何时使用该属性。
如何系统的优化页面
页面优化即让页面更快的显示和响应。
一个页面有三个阶段:加载阶段、交互阶段、关闭阶段。
加载阶段:请求发出到渲染完整页面,影响因素有网络
和JavaScript脚本
。
交互阶段:页面加载完成到用户交互,影响因素主要是JavaScript脚本
。
关闭阶段:主要是用户发出关闭指令后页面的一些清理操作。
- 加载阶段
- 造成阻塞:JS文件、首次加载的HTML文件、CSS文件等。
- 不会造成阻塞:图片、视频、音频等资源。
- 造成阻塞的原因
- 关键资源个数
- 关键资源大小
- 请求关键资源需要多少个RTT(round trip time)
- 优化策略
- 减少关键资源个数,使用如内联css和内联javascript
- 减小关键资源大小,去除css重复代码,javascript文件中注释和重复代码
- 减少rtt次数,一次rtt最多14kb的数据包传输,减小css和html和js文件体积大小。
- 交互阶段:主要考虑重排、重绘
- 减少JS执行时间
- 一次执行的函数分解为多步操作
- 使用
web worker
进行非dom操作的逻辑处理
- 避免强制同步布局
- css计算属性是在单独的任务中进行的
- 如mwui-vue中处理nav组件的动画使用到了js使css强制计算当前元素的css属性.
- 避免布局抖动
- 合理利用css合成动画(will-change属性)
- 避免频繁垃圾回收(尽可能减少闭包的使用)
- 减少JS执行时间
虚拟DOM
如果通过JS脚本直接修改DOM的话,会引起如,重排、重绘、合成,甚至因为不当操作,还会引起如,布局抖动和强制同步布局等一系列的操作。所以虚拟DOM,改进了上述的问题。
- 虚拟DOM
- 虚拟dom做了什么
- 将页面变更内容应用到虚拟DOM上,而不是直接修改DOM。
- 数据变化时,生成新的虚拟DOM,对比新旧虚拟DOM,找出变化的节点。
- 数据停止变化后,渲染到DOM上,更新页面
- 虚拟dom做了什么
- 几种设计模式
- 双缓存
- 虚拟DOM,类似双缓存中的Buffer(缓存)
- MVC
- Model(数据层)、View(视图层)、Controller(逻辑处理层)
- 双缓存
- react中的具体流程
- Controller 监控 DOM 变化,DOM变化,Controller 通知 Model 更新数据。
- Model 更新数据后,Controller通知 View ,告知数据发生改变。
- View 接受更新消息后,生成新的虚拟DOM。
- 新虚拟DOM生成好后,与旧的虚拟DOM比较(diff),找出变化节点。
- react此时将变化节点应用到DOM上,触发DOM节点的更新。
- DOM 节点的变化又会触发后续一系列渲染流水线的变化,从而实现页面的更新。
浏览器中的网络
HTTP/1
HTTP 是浏览器中最重要且使用最多的协议,是浏览器和服务器之间的通信语言,也是互联网的基石。
- HTTP/0.9
- 请求过程
- 客户端先要根据 IP 地址、端口和服务器建立 TCP 连接,而建立连接的过程就是 TCP 协议三次握手的过程。
- 建立好连接之后,会发送一个 GET 请求行的信息。
- 服务器接收请求信息之后,读取对应文件,并将数据以ASCII字符流返回给客户端
- 传输完成后,断开连接。
- 3个特点
- 只有一个请求行,并没有 HTTP 请求头和请求体,因为只需要一个请求行就可以完整表达客户端的需求了。
- 服务器也没有返回头信息,这是因为服务器端并不需要告诉客户端太多信息,只需要返回数据就可以了。
- 返回的文件内容是以 ASCII 字符流来传输的,因为都是 HTML 格式的文件,所以使用 ASCII 字节码来传输是最合适的。
- 请求过程
- HTTP/1.0 解决HTTP/0.9不支持多种不同类型的数据处理的问题.
- 首先,浏览器需要知道服务器返回的数据是什么类型,然后浏览器才能根据不同的数据类型做针对性的处理。
- 其次,服务器会对数据进行压缩后再传输,所以浏览器需要知道服务器压缩的方法。
- 再次,浏览器告诉服务器它想要什么语言版本的页面。
- 最后,浏览器需要知道文件的编码类型。
- 以上问题皆需要通过设置请求头去设置。
- HTTP/1.1 相比较HTTP/1.0的改进。
- 改进持久连接,HTTP/1.1 中增加了持久连接的方法,它的特点是在一个 TCP 连接上可以传输多个 HTTP 请求,只要浏览器或者服务器没有明确断开连接,那么该 TCP 连接会一直保持。
- 不成熟的 HTTP 管线化,TCP内只要有一段数据包传输出错,就会造成队头阻塞。
- 提供虚拟主机的支持
- 对动态生成的内容提供了完美支持
- 客户端 Cookie、安全机制
HTTP/2
__END__