一、虚拟节点的创建与渲染
我们知道,Vue框架是基于虚拟节点进行页面渲染的,虚拟节点保存着渲染实际dom元素相关的所有信息,整个Vue3应用就是一棵虚拟节点树的映射。在创建这棵节点树的过程中,一种类型为组件的虚拟节点,担负起了承上启下的作用。
简单从一个组件模板的编译转换来推测一个组件在渲染中实际做了什么:
假设左侧的模板是一个组件的模板内容,右侧为编译结果。这里有2个很重要的信息。
- 一个组件模板的编辑结果为一个render渲染函数
- render函数内部会创建模板中使用到的组件节点
这儿其实很隐藏着一个很重要的信息。一个render方法的本质是返回了一棵虚拟节点树,这棵虚拟节点树是整个应用的虚拟节点树上的一部分。 createElementVNode,createElementBlock,createVNode这些方法本质上就是创建一个虚拟节点。
因此整颗虚拟节点树的本质就是渲染组件节点,然后将各个组件节点生成的虚拟节点树拼接成为一棵应用节点树,如下:
// App组件
Component(App)
- div
- div
- Component(ComA)
- Component(ComB)
- span
// ComA组件
- div
- div
- span
// ComB组件
- div
- span
- span
// 应用虚拟节点树
- div(App)
- div
- div(ComA)
- div
- span
- div(ComB)
- span
- span
- span
对于熟练使用Vue进行开发的开发者而言,上面的这种转换结果想必都是比较了解的。
鉴于篇幅原因,这儿简要做个总结,Vue3框架内部的渲染主流程如下。
- 执行组件渲染函数,创建子节点
- 以深度优先的算法遍历子节点
- dom相关节点,直接渲染对应的dom元素
- 组件节点,执行第1步
- 其他节点(Fragment等),执行其他逻辑
通过上面的步骤,便能创建出一棵完整的虚拟节点树。
二、应用的创建-从初始节点开始
在上面介绍了虚拟节点树的创建主流程,但这里面其实存在着一个问题。那便是在render函数中才会创建子节点,但必须要先有一个组件节点,才能去执行render函数。
这个时候,就得提到我们的初始节点了,一般一个Vue项目,在通过Vite或者VueCli创建后,都会有一个App节点,这个节点便是初始节点,也叫做应用根节点。这个节点需要我们手动创建,并且手动触发渲染,执行其render函数,这是每个项目都必须存在的操作,只不过在外层API的封装下,作为开发者而言无法感知。简单代码如下:
import App from './App.vue';
// 创建应用,缓存根组件
const app = createApp(App);
// 挂载时创建根组件节点,并执行根组件节点渲染函数
app.mount('#app');
三、Patch-Vue3的更新流程
许多开发者可能听说过Patch这个概念,但具体指的是什么并不清楚,接下来我详细介绍这个概念。在Vue3中,某个响应式对象的变更会导致render函数的重新执行,因此会生成新的子节点树。
此时有2种更新方式:
- 将旧的子节点树直接从虚拟节点树中移除,然后加载新的子节点树
- 将新旧2棵子节点树进行比对,尽量复用旧的子节点
第一种性能较低,但逻辑简单清晰,第二种性能较高,但逻辑十分复杂。你可能在想,Vue3里面肯定采用的第二种,但实际上是二种都在使用,Vue3会尽可能的去复用节点,但并不是所有节点都是可以复用的。
作为Vue3项目的开发者而言,经常会和key属性打交道。当key不一致时,则表明这个节点是不一致的,即便除了key以外的其他属性全部一致,因为key相当于是节点的唯一标识。Vue3内部想要进行节点复用,首先节点类型需要一致,当然这个key也是必须要一致的。当新旧节点不一致时,则会走流程1,这样的场景包括v-if条件判断和动态key等,但对于绝大多数场景而言,都是走的流程2。
在流程2中,则会比较新旧2棵节点树,这个比较的过程,就叫做patch,主要是下面几个步骤:
- 比较节点
- Text 文本节点
- Comment 注释节点
- Static 静态节点 生产环境静态节点不会产生变化,因此生产环境不会比较静态节点
- Fragment 聚合节点 一组节点的逻辑父级节点,比如v-for和多根组件就会渲染一个Fragment节点
- Element dom元素节点
- Component 组件节点
- Teleport 内置Teleport组件
- Suspense 内置Suspense组件
- 比较属性
- 修改dom元素
针对不同类型的节点,会有不同的处理。下面介绍4种主要节点的处理:
- Text:文本节点直接修改文本内容。
- 元素节点: 修改dom属性值,且遍历比较其子节点。
- Fragment:这个节点本身不做任何dom渲染,因此直接比较其子节点。
- Component: 判断是否需要更新,如果需要,则走这个组件节点的Patch流程。
针对Component节点,这儿不要混淆了。当前在进行比较,已经在执行Patch流程,这就代表已经是某个组件的render触发了。但这个组件可能存在子组件,如果子组件的一切都是没变化的,则没必要执行子组件的render,如果子组件也有改动,则需要执行子组件的render。
四、Diff算法在Patch中的作用
在前面的章节,几乎把Vue3的创建和更新主流程讲述完毕。但在更新过程中,有一个Diff算法也是至关重要的。从前面知道,Patch流程本质上是一个组件的render重新执行后,进行更新前后节点树的比较流程,Diff算法就是在这个比较过程中的一个节点快速比较的算法。Vue3的Diff算法相对而言比较复杂,分很多种情况,后续单独出一篇博客讲解。