Vue渲染机制
真实DOM和虚拟DOM
- 虚拟DOM是将目标所需的UI通过数据结构虚拟表现出来,保存到内存中,再将真实DOM与之保持同步
- 过程
- 编译:渲染函数,即返回虚拟DOM树的函数
- 挂载:渲染器调用渲染函数,遍历返回的虚拟DOM树,构建真实DOM树
- 更新:当依赖发生变化,副作用重新运行,创建一个更新后的虚拟DOM树,渲染器比较地遍历新旧两颗虚拟DOM树,将其中的变化应用到真实的DOM上
- 虚拟DOM缺点
- 更新算法无法预知新的虚拟DOM树,总是需要遍历整棵树、比较每个 vnode 上 props 的区别来确保正确性
- 即使一棵树的某一部分从未改变,每次渲染时还是会创建新的vnode,给了内存很大的压力
- 编译时优化
- 在Vue中,框架同时控制着编译器和运行时
- 优化方案:带编译时信息的虚拟DOM
- 编译器可以静态分析模板并在生成的代码中留下标记,使运行时尽可能走捷径
带编译时信息的虚拟DOM
缓存静态内容
<div>
<div>foo</div> <!-- 需缓存 -->
<div>bar</div> <!-- 需缓存 -->
<div>{{ dynamic }}</div>
</div>
- 渲染器在首次渲染时会创建
foo
和bar
这两个div的vnode缓存起来,这部分的新旧vnode是完全相同的,渲染器会完全跳过对它们的差异比对,并且在后续的重新渲染中使用缓存的vnode - 但有足够多连续的静态元素时,还会把它们压缩为一个静态vnode,其中包含它们的纯HTML字符串,会直接通过innerHTML挂载
靶向更新(更新类型标记)
-
对于单个有动态绑定的元素,在编译时就会推断出大量信息,如元素的绑定信息、子节点信息
-
为有动态绑定的元素生成渲染函数时,Vue会在vnode创建调用中直接编码每个元素所需的更新类型
createElementVNode("div", { class: _normalizeClass({ active: _ctx.active }) }, null, 2 /* CLASS */)
- 参数2即为一个更新类型标记patch flag
- 一个元素可以有多个更新类型标记,会被合并成一个数字
-
运行时渲染器会使用位运算来检查这些标记,确定相应的更新操作
if (vnode.patchFlag & PatchFlags.CLASS /* 2 */) { // 更新节点的 CSS class }
-
Vue也为vnode的子节点标记了类型,包含多个根节点的模板被表示为一个片段fragment,大多数情况下其顺序不变,这部分信息可以提供给运行时作为一个更新类型标记
树结构打平
-
返回的虚拟DOM树是经
createElementBlock()
调用创建的export function render() { return (_openBlock(), _createElementBlock(_Fragment, null, [ /* children */ ], 64 /* STABLE_FRAGMENT */)) }
-
内部结构是稳定的一个部分可被称之为一个区块,每一个块都会追踪其所有带更新类型标记的后代节点(不只是直接子节点)
-
编译的结果会被打平为一个数组,仅包含所有动态的后代节点
div (block root) - div 带有 :id 绑定 - div 带有 {{ bar }} 绑定
-
当这个组件需要重渲染时,只需要遍历这个打平的树而非整棵树,大大减少了我们在虚拟 DOM 协调时需要遍历的节点数量
-
v-if
和v-for
指令会创建新的区块节点,一个子区块会在父区块的动态子节点数组中被追踪,这为他们的父区块保留了一个稳定的结构
对SSR激活的影响
SSR激活:将组件在服务端直接渲染成 HTML 字符串,作为服务端响应返回给浏览器,最后在浏览器端将静态的 HTML“激活”(hydrate) 为能够交互的客户端应用
-
单个元素的激活可以基于相应 vnode 的更新类型标记走更快的捷径。
返回给浏览器,最后在浏览器端将静态的 HTML“激活”(hydrate) 为能够交互的客户端应用 -
单个元素的激活可以基于相应 vnode 的更新类型标记走更快的捷径。
-
在激活时只有区块节点和其动态子节点需要被遍历,这在模板层面上实现更高效的部分激活
参考资料:
Vue渲染机制:https://cn.vuejs.org/guide/extras/rendering-mechanism
vue3早已具备抛弃虚拟DOM的能力了:https://mp.weixin.qq.com/s/jNj0JZMOFs2NXTNgnyhEfg