Vue3 的 diff 流程
-
新旧 VNode 对比的开始
- 触发更新机制:在 Vue 3 中,当组件的响应式数据发生变化时,会重新执行组件的渲染函数来创建新的虚拟节点(VNode)。然后,会启动 DOM diff 流程,将新的 VNode 和旧的 VNode 进行对比。这个过程是自动触发的,例如,当一个组件中的数据属性(如
data中的变量)被修改,Vue 3 的响应式系统会察觉到这个变化,进而驱动渲染更新和 DOM diff 过程。 - 根节点比较:首先会对新旧 VNode 的根节点进行比较。比较的内容包括节点的类型(如 HTML 标签类型
div、p等,或者是组件类型)、节点的属性(如class、id、style等)和节点的 key(如果有指定)。如果根节点的类型不同,例如旧的根节点是div,新的根节点是p,那么 Vue 3 会直接替换整个旧节点为新节点。
- 触发更新机制:在 Vue 3 中,当组件的响应式数据发生变化时,会重新执行组件的渲染函数来创建新的虚拟节点(VNode)。然后,会启动 DOM diff 流程,将新的 VNode 和旧的 VNode 进行对比。这个过程是自动触发的,例如,当一个组件中的数据属性(如
-
节点类型相同的情况
- 属性更新:如果新旧 VNode 的根节点类型相同,接下来会比较它们的属性。对于不同的属性,会进行相应的更新操作。例如,如果旧 VNode 的
class属性是old - class,新 VNode 的class属性是new - class,那么会更新 DOM 节点的class属性。同时,对于一些特殊的属性(如style属性),会进行更细致的比较和更新,因为style属性的值可能是一个复杂的对象。 - 子节点比较:在属性比较完成后,会对根节点的子节点进行比较。这是 DOM diff 流程中比较复杂的部分。如果子节点是文本节点,直接比较文本内容是否相同,若不同则更新文本内容。如果子节点是元素节点或者组件节点,会采用不同的策略。
- 属性更新:如果新旧 VNode 的根节点类型相同,接下来会比较它们的属性。对于不同的属性,会进行相应的更新操作。例如,如果旧 VNode 的
-
处理子节点的策略
- 同步头部节点
- 同步尾部节点
- 添加新的节点
- 删除多余节点
- 处理未知子序列
-
更新 DOM 操作的执行
- 批量更新机制:在完成新旧 VNode 的比较并确定了需要更新的 DOM 操作后,Vue 3 不会立即执行每个单独的更新操作,而是采用批量更新机制。这是为了减少浏览器的重排(reflow)和重绘(repaint)次数,提高性能。所有的更新操作会被收集起来,在一个合适的时机(如在下一个事件循环中)统一执行。
- 更新 DOM 元素:执行更新操作时,会根据比较的结果对 DOM 元素进行实际的更新。例如,更新节点的属性、插入新的节点、删除不需要的节点或者移动节点到新的位置等。这些操作完成后,DOM 就更新为与新的 VNode 对应的状态,从而使页面显示与组件的最新状态相匹配。
Vue3的DOM diff流程中用到了哪些算法?
-
双端比较算法(双指针法)
-
算法原理:
- 双端比较算法类似于双指针法,通过设置四个指针分别指向新旧子节点列表的头部和尾部。比较时,先比较头部和尾部节点,即旧子节点列表的头部节点与新子节点列表的头部节点、旧子节点列表的尾部节点与新子节点列表的尾部节点。
-
执行步骤:
- 初始化指针:为旧子节点列表设置头部指针
startIdxOld和尾部指针endIdxOld,初始值分别为 0 和旧子节点列表长度 - 1。为新子节点列表设置头部指针startIdxNew和尾部指针endIdxNew,初始值分别为 0 和新子节点列表长度 - 1。 - 头部和尾部节点比较:比较旧子节点列表头部节点和新子节点列表头部节点,如果相同则复用该节点,将
startIdxOld和startIdxNew分别向后移动一位;接着比较旧子节点列表尾部节点和新子节点列表尾部节点,如果相同则复用该节点,将endIdxOld和endIdxNew分别向前移动一位。 - 终止条件:当新节点头部和旧节点头部不同时,或者新节点尾部和旧节点尾部不同时,算法比对结束,进入下个阶段。
- 初始化指针:为旧子节点列表设置头部指针
-
-
基于最长递增子序列(LIS)和二分法的算法(用于优化节点移动操作)
-
算法原理:
- 最长递增子序列(LIS)是在一个序列中找到一个递增的子序列,且这个子序列是所有递增子序列中最长的。在 Diff 算法中,将新旧子节点列表看作序列,通过寻找最长递增子序列来确定哪些节点的相对顺序没有改变或者可以通过最小的移动来恢复顺序。二分法用于高效地寻找最长递增子序列,降低时间复杂度。
-
执行步骤:
- 建立索引和映射关系(如果有) :为新旧子节点列表中的每个节点建立索引。如果节点有
key属性,可以创建一个映射表来记录新旧节点基于key的对应关系。 - 初始化辅助数组和相关变量:创建一个空的辅助数组
tails用于记录最长递增子序列的末尾元素相关信息,长度初始化为 0。同时创建一个长度与新子节点列表相同的prev数组,用于记录每个新节点在最长递增子序列中的前驱节点索引,初始值设为 - 1。 - 遍历新子节点列表(结合二分法) :从新子节点列表的第一个节点开始,对于每个节点,使用二分法在
tails数组中查找可以插入该节点索引的位置。如果插入位置等于tails数组的长度,就将该节点索引添加到tails数组末尾,并在prev数组中记录前驱节点索引;否则,更新tails数组中该插入位置的元素为当前节点索引,并在prev数组中记录前驱节点索引。 - 构建最长递增子序列结果:通过
prev数组回溯构建最长递增子序列对应的节点索引列表。从tails数组最后一个元素对应的索引开始,依次将索引放入结果列表,根据prev数组中记录的前驱节点索引更新当前索引,直到索引为 - 1。 - 节点复用和移动判断:对于最长递增子序列中的节点,根据索引找到对应的新旧节点进行复用。比较这些节点在新旧子节点列表中的位置,位置不同则标记为需要移动。对于不在最长递增子序列中的新子节点,判定为新增节点;对于不在最长递增子序列中的旧子节点,判定为删除节点。
- 建立索引和映射关系(如果有) :为新旧子节点列表中的每个节点建立索引。如果节点有
-
-
基于节点
key属性的算法(贯穿整个 Diff 过程)-
算法原理:
- 节点的
key属性在 Diff 算法中起到了精准识别节点的作用。在比较新旧子节点时,先根据key属性查找是否有对应的节点。如果新子节点的key在旧子节点列表中有对应的节点,且节点类型相同,就优先复用这个节点,然后再根据节点位置等情况判断是否需要移动。
- 节点的
-
执行步骤:
- 查找匹配节点:在比较新旧子节点时,对于新子节点,查找其
key在旧子节点列表中是否有对应的节点。如果有,检查节点类型是否相同。 - 节点复用和位置判断:如果找到相同
key且节点类型相同的节点,就复用这个节点。然后比较其在新旧子节点列表中的位置,如果位置不同,标记为需要移动的节点。如果新子节点的key在旧子节点列表中找不到对应的节点,判定为新增节点;如果旧子节点的key在新子节点列表中找不到对应的节点,判定为删除节点。
- 查找匹配节点:在比较新旧子节点时,对于新子节点,查找其
-
-
文本节点比较算法(相对简单)
-
算法原理:
- 当子节点是文本节点时,直接比较新旧文本节点的内容是否相同。
-
执行步骤:
- 对于新旧子节点列表中的文本节点,直接比较它们的文本内容。如果内容不同,就更新 DOM 中对应的文本节点内容。
-
-
综合执行流程
- 根节点比较:首先比较新旧 VNode 的根节点,包括节点类型、属性等。如果根节点类型不同,直接替换整个旧节点为新节点。如果根节点类型相同,更新根节点的属性。
- 子节点比较:对于根节点的子节点,先使用双端比较算法进行初步比较和节点复用、新增和删除节点的判断。基于最长递增子序列和二分法的算法进一步优化节点移动操作。在整个子节点比较过程中,始终结合节点的
key属性来精准地判断节点的复用和更新情况。对于文本节点,按照文本节点比较算法单独处理。 - 更新 DOM 操作的执行:在完成新旧 VNode 的比较并确定了需要更新的 DOM 操作后,会采用批量更新机制,将更新操作收集起来,在合适的时机统一执行,更新 DOM 元素,使 DOM 与新的 VNode 状态相匹配。
Vue3 的 diff 算法 VS Vue2 的 diff 算法
-
更高效的双端比较算法(双指针法)
- 充分利用 key 属性:Vue3 在双端比较算法中更加充分地利用了节点的
key属性。在比较新旧子节点列表时,除了常规的头部和尾部节点比较外,在交叉比较(旧头 - 新尾,旧尾 - 新头)阶段,如果没有找到匹配的节点,会优先根据key属性来查找是否有可复用的节点。这使得在处理节点顺序变化、插入和删除操作时更加高效。例如,在一个动态列表中,当列表项的顺序被打乱或者有新的列表项插入时,Vue3 能够通过key快速定位到对应的节点,准确地判断是需要移动节点还是插入 / 删除节点,减少了不必要的 DOM 操作。
- 充分利用 key 属性:Vue3 在双端比较算法中更加充分地利用了节点的
-
编译阶段对静态节点的优化
- 静态节点标记:Vue3 在编译阶段能够更准确地识别静态节点,并在生成虚拟节点(VNode)时对其进行标记。静态节点是指在组件的生命周期内不会发生变化的节点,如一些固定的文本、样式等。通过在编译阶段的分析,Vue3 可以区分出这些静态节点。
- 跳过 Diff 操作:在后续的 Diff 算法执行过程中,对于标记为静态的节点,直接跳过 Diff 操作,因为它们不会发生变化。例如,对于一个组件模板中有一个固定的标题文本(静态节点)和一个动态更新的数据显示部分,在 Diff 算法执行时,只会对动态数据显示部分对应的 VNode 进行比较和更新,而标题文本对应的静态节点则不会参与 Diff,从而提高了更新效率。
-
支持 Fragment(片段)的优化
- 灵活的模板结构:Vue3 支持 Fragment(片段),这使得组件模板可以没有根元素,直接返回多个节点。这在一些场景下能够更灵活地编写组件模板,并且可以避免生成不必要的 DOM 元素。例如,在一个组件中,只需要返回几个并列的按钮或者文本节点,在 Vue3 中可以直接返回这些节点,而不需要一个额外的根元素包裹。
- 优化 Diff 操作:在 Diff 算法执行时,对于这种 Fragment 类型的 VNode,能够更直接地比较和更新节点,减少了因为根元素限制而产生的额外操作。与 Vue2 必须有根元素的模板结构相比,Vue3 的 Fragment 支持使得 Diff 算法在处理这类模板时更加高效,避免了不必要的节点嵌套和比较。
-
响应式系统改进对 Diff 算法的间接优化
- 采用 Proxy 实现响应式:Vue3 采用
Proxy来实现数据的响应式。Proxy能够更全面地代理对象,对于嵌套对象和数组的处理更加自然和高效。在 Diff 算法执行时,由于响应式系统能够更及时、准确地捕捉数据变化,使得 Diff 算法能够更好地基于最新的数据状态进行操作。 - 更精准的更新触发:与 Vue2 使用
Object.defineProperty实现响应式不同,Vue3 的Proxy- 基于的响应式系统可以更精准地将数据变化通知给 Diff 算法。例如,对于复杂的嵌套对象和数组的更新,Vue3 的响应式系统能够自动地将数据变化通知给 Diff 算法,Diff 算法可以根据新的数据状态更精准地进行 VNode 的比较和 DOM 更新,减少了因为响应式系统问题导致的更新异常情况。
- 采用 Proxy 实现响应式:Vue3 采用
Vue3 中 key 值的作用
-
高效的 DOM 更新
- 节点复用与精准定位:在 Vue 3 的虚拟 DOM(VNode)的 Diff 算法中,
key值起到了关键的作用。当比较新旧子节点列表时,会先根据key来查找新子节点在旧子节点列表中是否有对应的节点。如果新子节点的key与旧子节点列表中的某个节点的key相同,并且节点类型也相同,那么会优先复用这个节点。例如,在一个动态列表组件中,列表项可能会频繁地进行添加、删除或者重新排序操作。假设列表项是用户信息,每个列表项都有一个唯一的userID作为key。当列表的顺序发生变化时,Vue 3 通过key可以快速地定位到每个用户信息对应的旧节点,从而复用这些节点,而不是重新创建所有节点。 - 减少不必要的 DOM 操作:没有
key或者key使用不当可能会导致 Vue 在更新 DOM 时出现错误的节点复用或者不必要的 DOM 更新。例如,如果在一个循环渲染的列表中不使用key,当列表项的顺序发生变化时,Vue 可能会错误地认为所有的节点都需要重新创建,而不是正确地移动和复用节点。而正确使用key可以避免这种情况,使得 DOM 更新更加高效。例如,对于一个包含大量数据的表格组件,每个表格行都有一个唯一的key(如行数据的主键),在数据更新时,Vue 3 能够通过key精准地更新变化的行,减少不必要的表格行重新渲染,提高性能。
- 节点复用与精准定位:在 Vue 3 的虚拟 DOM(VNode)的 Diff 算法中,
-
组件状态的保持与正确更新
- 组件实例与
key关联:在 Vue 3 中,key还与组件实例相关联。当一个组件在模板中多次出现,并且key不同时,Vue 会将它们视为不同的组件实例。这对于组件状态的管理非常重要。例如,有一个可复用的表单组件,在一个页面中可能会出现多个相同类型的表单用于不同的用户信息编辑。如果每个表单组件都有一个唯一的key(如与用户 ID 相关联),那么它们各自的状态(如表单输入框中的值、验证状态等)会独立维护,不会相互干扰。当某个表单组件的数据发生变化时,Vue 会根据其key正确地更新对应的组件实例,而不会影响其他具有不同key的表单组件。 - 动态组件更新中的
key作用:在动态组件(通过component标签和:is属性来动态切换组件)的更新过程中,key也很重要。如果动态组件的key发生变化,Vue 会销毁旧的组件实例,并创建新的组件实例。这可以确保在组件切换时,组件能够正确地重新初始化和更新。例如,有一个页面根据用户权限动态显示不同的组件,当用户权限发生变化时,通过改变动态组件的key,可以确保新的组件能够根据新的权限设置正确地初始化和加载数据,而旧的组件实例被销毁,释放资源。
- 组件实例与
diff 算法之前,Vue3 还做了哪些优化?
-
编译阶段的优化
-
静态节点提升:
- 原理:在编译阶段,Vue3 能够识别模板中的静态节点(即那些在组件的生命周期内不会改变的节点)。这些静态节点会被提升到渲染函数之外,这样在每次组件重新渲染时,就不需要重新创建和比较这些静态节点的虚拟节点(VNode)。例如,对于一个组件模板中固定的标题文本或者图标等静态元素,它们会被单独处理。
- 优势:通过静态节点提升,减少了 Diff 算法需要处理的节点数量,从而提高了渲染效率。因为 Diff 算法主要关注的是动态变化的节点,静态节点提前处理可以避免不必要的计算和比较。
-
事件缓存:
- 原理:Vue3 会在编译阶段对模板中的事件处理函数进行缓存。当组件重新渲染时,不需要重新绑定事件处理函数,除非事件处理函数本身发生了变化。例如,对于一个按钮的点击事件处理函数,在第一次渲染绑定后,后续渲染时如果函数没有改变,就直接使用缓存的绑定。
- 优势:这种事件缓存机制减少了在组件更新过程中对事件处理部分的操作,提高了性能。因为在 JavaScript 中,频繁地重新绑定事件处理函数会带来一定的性能开销,事件缓存避免了这种不必要的开销。
-
块级作用域变量提升:
- 原理:在编译模板生成渲染函数时,Vue3 会对块级作用域内的变量进行合理的提升。这样可以减少变量查找的层级和范围,提高渲染函数的执行效率。例如,在一个带有
v - if和v - for等指令的复杂模板中,内部块级作用域的变量能够更高效地被访问。 - 优势:通过优化变量访问,使得渲染函数在执行过程中能够更快地获取所需的数据,减少了查找变量所花费的时间,从而间接提高了整个渲染过程的速度。
- 原理:在编译模板生成渲染函数时,Vue3 会对块级作用域内的变量进行合理的提升。这样可以减少变量查找的层级和范围,提高渲染函数的执行效率。例如,在一个带有
-
-
响应式系统的优化
-
Proxy - 基于的响应式实现优势:
- 原理:Vue3 采用
Proxy来实现数据的响应式,相较于 Vue2 使用Object.defineProperty有很大的改进。Proxy可以代理整个对象,包括对象的属性添加、删除等操作,对于嵌套对象和数组的处理更加自然。例如,对于一个嵌套的对象结构,Proxy能够自动地将内部对象的变化也捕获到,不需要像 Vue2 那样对数组的一些变异方法进行特殊处理。 - 优势:这种响应式系统的优化使得数据变化的检测更加及时和准确。在 Diff 算法执行之前,能够确保数据的变化已经被正确地捕获,为 Diff 算法提供了更可靠的数据基础,使得 Diff 算法可以基于最新的、完整的数据状态进行比较和更新操作,减少了因为数据更新不及时或不完全导致的视图更新异常情况。
- 原理:Vue3 采用
-
细粒度的响应式更新:
- 原理:Vue3 的响应式系统能够更精准地确定哪些数据发生了变化,从而只触发与这些变化相关的组件更新。例如,在一个大型组件中,如果只有一个小部分的数据发生了变化,Vue3 可以只更新与这个数据相关的子组件或者 DOM 区域,而不是像 Vue2 那样可能会引起整个组件的重新渲染。
- 优势:这种细粒度的更新方式减少了不必要的组件重新渲染和 Diff 操作。在 Diff 算法之前,就已经对更新范围进行了限制,使得 Diff 算法需要处理的内容减少,提高了更新效率。
-