《深入浅出Vue.js》
# ⭕️ 第二篇 虚拟 DOM
# 什么是虚拟 DOM
🔥🔥虚拟 DOM 是将状态映射成视图的众多解决方案之一
虚拟 DOM 的解决方式是通过状态生成一个虚拟节点树 🌲,然后使用虚拟节点树进行渲染。 在渲染之前,会使用新生成的寻你节点树和上一次生成的虚拟节点树进行对比,只渲染不同的部分。
# 为什么要引入虚拟 DOM
Angular 和 react 的变换侦测都不知道哪些状态变了,因此需要进行暴力比对。
- Angular 使用脏检查
- React 使用虚拟 DOM
Vue 的变化侦测和上两者不同,因为 vue 知道具体哪些状态发生了变化,这样就可以通过更细的粒度来绑定更新视图。
- Vue.js 1.0 无需比对直接对更新节点操作 (无虚拟 DOM 操作)
- 因为粒度太细,每一个绑定的状态都有一个相应的 watcher 来观察
- 当状态被越多的节点使用时开销会变得极大
- Vue.js 2.0 选择中等粒度的解决方案 —— 引入虚拟 DOM
- 一个 watcher 实例仅观察一个组件
- 🔥状态变化时 watcher 通知组件,组件内通过虚拟 DOM 去进行比对与渲染
# Vue.js 中的虚拟 DOM
模板 ⏩ 渲染函数 ⏩ vnode(patch) ⏩ 视图
- 模板:我们使用模板来描述状态与 DOM 之间的映射关系 如
{{}}
语法 - Vue.js 通过编译将模板转换成渲染函数 render
- 执行 render 即可得到虚拟节点树 🌲
- 使用虚拟节点树进行Patch 算法后即可更新视图
- 🔥Patch 算法详见下文
# ----------
# 什么是 VNode
Vue.js 中存在一个 vnode 类,使用它可以实例化不同类型的 vnode 实例,而不同类型的 vnode 实例各自表示不同类型的 DOM 元素
export default class VNode{
constructor (tag,data,children,text,elm...){
this.tag = tag
...
}
}
2
3
4
5
6
🔥简单地说,vnode 可以理解成节点描述对象,它描述了怎样去创建真实的 DOM 节点。
# VNode 的作用
🔥将上一次渲染视图时创建的 vnode 缓存起来,重新渲染时进行比对
# VNode 类型
- 注释节点
<!-- 注释节点 -->
- 文本节点
- 元素节点
- tag:p,ul,li,div...
- data:class,id...
- 组件节点
- componentOptions
- componentInstance
- 函数式组件
- functionalContext
- functionalOptions
- 克隆节点:优化静态节点和插槽节点
- 将现有节点全部属性复制到新节点即可
- isCloned = true
# ----------
# Patch 介绍
🔥🔥创建 ➕ 删除 ➕ 更新节点
🔁运行流程:
- oldVnode 是否存在 ➡️ ❎ 不存在则使用 vnode 创建节点插入视图
- ✅ 存在 检查 OldVnode 和 Vnode 是否为同一节点 ➡️ ✅ 是则使用 patchVnode 进行更新
- ❎ 不是则使用 vnode 创建真实节点插入视图 并删除旧节点
- ✅ 存在 检查 OldVnode 和 Vnode 是否为同一节点 ➡️ ✅ 是则使用 patchVnode 进行更新
# 创建节点
⚠️ 只有三种节点会被插入 DOM 中:元素节点、注释节点、文本节点
判断顺序:
- 是否为元素节点
- 依据
vnode.tag
- 🔥如何创建真实元素节点:
- 1️⃣ 调用
createElement
创建元素节点 - 2️⃣ 循环 vnode 的 children 属性
- 3️⃣ 调用
appendChild
插入父节点
- 1️⃣ 调用
- 依据
- 是否为注释节点
- 依据
vnode.isComment
- 调用
createComment
- 依据
- 文本节点
- 调用
createTextNode
- 调用
# 删除节点
逻辑较为简单
# 更新节点
更新并不是暴力的用新节点覆盖旧节点,而是通过比对找出不一样的地方进行更新。
# 静态节点
⚠️ 更新节点时首先判断是否为静态节点,如果是则不需要进行操作。
例如<p>12345</p>
没有绑定状态的节点即为静态节点,一旦被渲染后不会发生变化。
更新节点算法将跳过类似节点。
# 🔥 具体流程图
# 更新子节点【详解】
# 更新策略
- 创建子节点:插入到所有未处理之前
- 更新子节点
- 移动子节点:
Node.insertBefore()
插入所有未处理之前 - 删除子节点:循环完 new 后 old 中还存在未处理的节点即可删除
# 优化策略
针对位置不变或者说位置可以预测的节点,我们不需要循环来查找,因为我们有一个更快捷的查找方式。
⚠️4 种快捷查找方式
- 新前 🆚 旧前 : 更新
- 新后 🆚 旧后 : 更新
- 新后 🆚 旧前 : 更新+移动
- 新前 🆚 旧后 : 更新+移动
# 🔥 哪些节点是未处理过的
设置四个变量oldStartIdx oldEndIdx newStartIdx newEndIdx
每处理一个节点则向相应方向移动一个位置。
结束循环条件:old 或 new 中任意一个的 idx 开始位置大于结束位置即停止
- old 提前结束:new 中剩下的节点都为需要新增的节点
- new 提前结束:old 中剩下的都是舍弃的节点
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
//循环体
}
2
3
# ⭕️ 第三篇 模板编译原理
# 模板编译介绍
模板编译的主要目标是生成渲染函数,每次执行渲染函数就会使用当前最新的状态生成一份新 node
🔥逻辑上分为三步:
- 将模板解析为 AST(解析器)
- 遍历 AST 标记静态节点(优化器)
- 使用 AST 生成渲染函数(代码生成器)
# 解析器
在解析器内部,分成了很多小解析器,其中包括过滤器解析器、文本解析器和 HTML 解析器,然后通过一条主线将这些解析器组装在一起
- 过滤器解析器
- 文本解析器
hello { {name}}
- HTML 解析器
如何解析详解见下文
# 优化器
优化器的目标是遍历 AST,检测出所有静态子树并给其打标记
# 代码生成器
模板编译的最后一步,将 AST 转换成渲染函数中的内容,该内容称为“代码字符串”
# ----------
# 解析器内部运行原理
# 钩子函数
parseHTML(template, {
start(tag, attrs, unary) {
// 标签开始 标签名 属性 是否自闭合
},
end() {
// 标签结束
},
chars(text) {
// 解析文本
},
comment(text) {
// 解析注释
}
});
2
3
4
5
6
7
8
9
10
11
12
13
14
# 栈管理 AST 层级关系
🔥 一个 AST 节点具有父节点和子节点,我们维护一个栈用来记录 DOM 的深度,每次触发钩子函数 start,把当前节点推入栈,触发 end 时弹出一个节点。