渲染机制 - 虚拟 DOM - vue 进阶主题
渲染机制
Vue 是如何将一份模板转换为真实的 DOM 节点的呢?又是如何高效地更新 DOM 节点的呢?我们接下来就将尝试通过深入研究 Vue 的内部渲染机制来解释这些问题。
虚拟 DOM
你可能已经听说过虚拟 DOM 的概念了,Vue 的渲染系统正是基于这个概念构建的。
虚拟 DOM(Virtual DOM,简称 VDOM)是一种编程概念,意为将将目标所需的 UI 通过数据结构“虚拟”地表示出来,保存在内存中,并与真实的 DOM 保持同步。这个概念是由 React 率先开拓,随后在各个不同的框架中都有不同的实现,当然也包括 Vue。
与其说虚拟 DOM 是一种具体的技术,不如说是一种模式。所以没有一个标准的实现。我们可以用一个简单的例子来说明:
const vnode = { type: 'div', props: { id: 'hello' }, children: [ /* 更多 vnode */ ] }
这里所说的 vnode 即一个纯 JavaScript 的对象(一个“虚拟节点”),它代表着一个 一个运行时渲染器将会遍历整个虚拟 DOM 树,并据此构建真实的 DOM 树。这个过程被称为挂载(mount)。 如果我们有两份虚拟 DOM 树,渲染器将会有比较地遍历它们,找出它们之间的区别,并应用这其中的变化到真实的 DOM 上。这个过程被称为更新(patch),又被称为“比较差异(diffing)”或“协调(reconciliation)”。 虚拟 DOM 带来的主要收益是它让开发者能够灵活、声明式地创建、检查和组合所需 UI 的结构,同时只需把具体的 DOM 操作留给渲染器去处理。 以更高层面的视角看,Vue 组件挂载后发生了如下这几件事: Vue 模板会被预编译成虚拟 DOM 渲染函数。Vue 也提供了 API 使我们可以不使用模板编译,直接手写渲染函数。在处理高度动态的逻辑时,渲染函数相比于模板更加灵活,因为你可以完全地使用 JavaScript 来构造你想要的 vnode。 那么为什么 Vue 默认推荐使用模板呢?有以下几点原因: 在实践中,模板对大多数的应用场景都是够用且高效的。渲染函数一般只会在需要处理高度动态渲染逻辑的可重用组件中使用。 虚拟 DOM 在 React 和大多数其他实现中都是纯运行时的:协调算法无法预知新的虚拟 DOM 树会是怎样,因此它总是需要遍历整棵树、比较每个 vnode 上 props 的区别来确保正确性。另外,即使一棵树的某个部分从未改变,还是会在每次重渲染时创建新的 vnode,带来了完全不必要的内存压力。这也是虚拟 DOM 最受诟病的地方之一:这种有点暴力的协调过程通过牺牲效率来换取可声明性和正确性。 但实际上我们并不需要这样。在 Vue 中,框架同时控制着编译器和运行时。这使得我们可以为紧密耦合的模板渲染器应用许多编译时优化。编译器可以静态分析模板并在生成的代码中留下标记,使得运行时尽可能地走捷径。与此同时,我们仍旧保留了边界情况时用户想要使用底层渲染函数的能力。我们称这种混合解决方案为带编译时信息的虚拟 DOM。 下面,我们将讨论一些 Vue 编译器用来提高虚拟 DOM 运行时性能的主要优化: 在模板中常常有部分内容是不带任何动态绑定的: 此外,当有足够多连续的静态元素时,它们还会再被压缩为一个“静态 vnode”,其中包含的是这些节点相应的纯 HTML 字符串。(示例)。这些静态节点会直接通过 对于单个有动态绑定的元素来说,我们可以在编译时推断出大量信息: 在为这些元素生成渲染函数时,Vue 在 vnode 创建调用中直接编码了每个元素所需的更新类型: 最后这个参数 位运算检查是非常快的。通过这样的修补标记,Vue 能够在更新带有动态绑定的元素时做最少的操作。 Vue 也为 vnode 的子节点标记了类型。举个例子,包含多个根节点的模板被表示为一个片段(fragment),大多数情况下,我们可以确定其顺序是永远不变的,所以这部分信息就可以提供给运行时作为一个修补标记。 运行时会完全跳过对这个根片段中子元素顺序的重新协调过程。 再来看看上面这个例子中生成的代码,你会发现所返回的虚拟 DOM 树是经一个特殊的 这里我们引入一个概念“区块”,内部结构是稳定的一个部分可被称之为一个区块。在这个用例中,整个模板只有一个区块,因为这里没有用到任何结构性指令(比如 每一个块都会追踪其所有带修补标记的后代节点(不只是直接子节点),举个例子: 编译的结果会被打平为一个数组,仅包含所有动态的后代节点: 当这个组件需要重渲染时,只需要遍历这个打平的树而非整棵树。这也就是我们所说的树结构打平,这大大减少了我们在虚拟 DOM 协调时需要遍历的节点数量。模板中任何的静态部分都会被高效地略过。 一个子区块会在父区块的动态子节点数组中被追踪,这为他们的父区块保留了一个稳定的结构。 修补标记和树结构打平都大大提升了 Vue SSR 激活的性能表现:渲染管线
模板 vs.渲染函数
带编译时信息的虚拟 DOM
静态提升
foo
和bar
这两个 div 是完全静态的,没有必要在重新渲染时再次创建和比对它们。Vue 编译器自动地会提升这部分 vnode 创建函数到这个模板的渲染函数之外,并在每次渲染时都使用这份相同的 vnode,渲染器知道新旧 vnode 在这部分是完全相同的,所以会完全跳过对它们的差异比对。innerHTML
来挂载。同时还会在初次挂载后缓存相应的 DOM 节点。如果这部分内容在应用中其他地方被重用,那么将会使用原生的cloneNode()
方法来克隆新的 DOM 节点,这会非常高效。修补标记 Flags
createElementVNode("div", {
class: _normalizeClass({ active: _ctx.active })
}, null, 2 /* CLASS */)
2
就是一个修补标记(patch flag)。一个元素可以有多个修补标记,会被合并成一个数字。运行时渲染器也将会使用位运算来检查这些标记,确定相应的更新操作:if (vnode.patchFlag & PatchFlags.CLASS /* 2 */) {
// 更新节点的 CSS 类
}
export function render() {
return (_openBlock(), _createElementBlock(_Fragment, null, [
/* children */
], 64 /* STABLE_FRAGMENT */))
}
树结构打平
createElementBlock()
调用创建的:export function render() {
return (_openBlock(), _createElementBlock(_Fragment, null, [
/* children */
], 64 /* STABLE_FRAGMENT */))
}
v-if
或者v-for
)。div (block root)
- div 带有 :id 绑定
- div 带有 {{ bar }} 绑定
v-if
和v-for
指令会创建新的区块节点:对 SSR 激活的影响
鹏仔微信 15129739599 鹏仔QQ344225443 鹏仔前端 pjxi.com 共享博客 sharedbk.com
图片声明:本站部分配图来自网络。本站只作为美观性配图使用,无任何非法侵犯第三方意图,一切解释权归图片著作权方,本站不承担任何责任。如有恶意碰瓷者,必当奉陪到底严惩不贷!