Vue3渲染原理

41 阅读3分钟

Vue3的渲染核心是响应式系统虚拟 DOM (Virtual DOM) 。它的渲染过程可以概括为以下几个关键阶段:挂载 (Mount)打补丁 (Patch)更新 (Update)

1. 响应式系统(Reactivity System)

在渲染流程开始之前,必须理解 Vue 3 如何知道数据发生了变化。

  • 核心机制:Proxy & Track/Trigger

    • Vue 3 使用 ES6 Proxy 来劫持数据的读写操作,替换了 Vue 2 的 Object.defineProperty
    • 当组件 首次渲染 时,会读取响应式数据,Proxy 会追踪 (Track) 依赖,记录下“哪个组件的哪个 effect (副作用,即渲染函数)”依赖了这个数据。
    • 当响应式数据被 修改 时,Proxy 会拦截到写入操作,并触发 (Trigger) 之前记录的所有依赖(即运行相关的 effect 函数)。
  • 与渲染的关系: 当数据变化触发 effect 时,这个 effect 就会调度执行组件的更新函数 (render function) ,从而进入后续的 VDOM 渲染和 Diff 流程。

2. 虚拟 DOM (Virtual DOM - VDOM)

渲染的中间层,是一个轻量级的 JavaScript 对象树,它描述了真实 DOM 树的结构。

  • VNode (Virtual Node): VDOM 树的每个节点都是一个 VNode 对象。

    • 它包含了描述真实 DOM 元素所需的一切信息,例如:

      • type: 元素的类型 (如 'div', 'p') 或组件定义。
      • props: 元素的属性或组件的 props。
      • children: 子 VNode 数组。
      • key: 用于 Diff 算法的唯一标识。
      • el: 对应的真实 DOM 节点 (在挂载后)。
  • 作用: 将复杂的 DOM 操作抽象化,让 Diff 算法可以直接在 JS 对象上进行比较,最大程度地减少直接操作真实 DOM 的次数,因为 DOM 操作的性能开销最大。

3. 渲染流程:挂载 (Mount) 与 打补丁 (Patch)

Vue 3 的渲染流程可以分为 首次渲染 (Mount)后续更新 (Patch) 两种情况。

🔹 首次渲染(Mounting)

  1. 执行 Setup/Render 函数: 运行组件的 setup 函数,并执行编译后的 渲染函数 (render function)
  2. 生成 VDOM 树 (VNode Tree): 渲染函数返回一个完整的 新的 VDOM 树 (New VNode Tree)
  3. VNode \to 真实 DOM: 框架调用 mount 过程,遍历新的 VNode 树,递归地创建对应的真实 DOM 节点 (document.createElement)。
  4. 关联并插入: 将真实 DOM 节点存储到 VNode 的 el 属性上,并将整个 DOM 结构插入到容器元素中。

🔹 后续更新(Patching)

当响应式数据变化时,会触发组件的更新函数,进入 Patch (打补丁) 流程。

  1. 生成新的 VDOM 树 (New VNode Tree): 再次执行渲染函数,生成一个代表当前状态的 新的 VDOM 树
  2. 获取旧 VDOM 树 (Old VNode Tree): 使用上一次渲染生成的 VDOM 树作为旧树
  3. Diff 算法比较: 框架调用 patch 过程,使用 Diff 算法新 VNode旧 VNode 进行逐层递归比较。
  4. 最小化 DOM 操作: Diff 算法会找出两棵树的差异 (即“补丁”)。
  5. 应用补丁: 框架根据这些差异,只对真实 DOM 中需要修改的部分进行精确的操作(如更新文本、修改属性、移动/删除/创建元素),而不是重新创建整个 DOM。

🌟 性能优化:静态提升 (Static Hoisting) 与 Block Tree

Vue 3 在编译阶段做了大量的优化,生成 VNode 时会跳过那些不会变化的静态内容(静态提升),并且只追踪和比较 VNode 树中动态绑定的部分(Block Tree)。这使得 Vue 3 的 Diff 效率远高于纯粹的 VDOM 比较。


🖼️ Vue 3 渲染流程图

以下是 Vue 3 从数据变化到最终屏幕更新的完整流程图:

graph TD
    A[开始: createApp] --> B[创建应用实例]
    B --> C[mount挂载]
    C --> D[创建根组件实例]
    
    D --> E[初始化组件]
    E --> E1[处理 props]
    E --> E2[处理 slots]
    E --> E3[执行 setup 函数]
    
    E3 --> F[创建响应式数据]
    F --> F1[reactive/ref API]
    F1 --> F2[Proxy 代理对象]
    
    E --> G[编译模板]
    G --> G1{是否有模板?}
    G1 -->|有| G2[模板编译为 render 函数]
    G1 -->|无| G3[使用 render 函数]
    G2 --> H[执行 render 函数]
    G3 --> H
    
    H --> I[依赖收集阶段]
    I --> I1[访问响应式数据]
    I1 --> I2[触发 getter]
    I2 --> I3[收集副作用函数 effect]
    
    H --> J[生成虚拟 DOM VNode]
    J --> J1[VNode 树结构]
    J1 --> J2[包含静态标记 PatchFlag]
    
    J --> K{首次渲染?}
    K -->|是| L[挂载 mount]
    K -->|否| M[更新 patch]
    
    L --> L1[创建真实 DOM]
    L1 --> L2[处理子节点]
    L2 --> L3[插入到容器中]
    L3 --> L4[执行 mounted 钩子]
    L4 --> N[渲染完成]
    
    M --> M1[Diff 算法比较]
    M1 --> M2[比较 VNode 类型]
    M2 --> M3{节点类型相同?}
    M3 -->|是| M4[复用节点, 更新属性]
    M3 -->|否| M5[替换节点]
    
    M4 --> M6[对比子节点]
    M6 --> M7[使用 key 优化]
    M7 --> M8[最长递增子序列算法]
    M8 --> M9[最小化 DOM 操作]
    
    M5 --> M9
    M9 --> M10[执行 updated 钩子]
    M10 --> N
    
    N --> O{数据是否变化?}
    O -->|是| P[触发 setter]
    O -->|否| Q[等待交互]
    
    P --> P1[通知依赖的 effect]
    P1 --> P2[将组件加入更新队列]
    P2 --> P3[异步批量更新 nextTick]
    P3 --> H
    
    Q --> R{用户交互?}
    R -->|是| S[事件处理]
    R -->|否| Q
    S --> T[更新数据]
    T --> P
    
    style A fill:#e1f5e1
    style N fill:#e1f5e1
    style F2 fill:#fff4e1
    style J1 fill:#fff4e1
    style M8 fill:#e1f0ff
    style P3 fill:#ffe1e1