Skip to content

在 Vue 3 的渲染系统中,render 函数和 VNode(虚拟节点)是构建用户界面的核心。理解它们的工作原理,是掌握 Vue 响应式更新、组件化和性能优化的基础。本节将深入剖析 render 函数的执行过程,解析 h() 函数的返回值,并揭示 VNode 如何精确描述一个组件。

一、从模板到 DOM 的旅程

Vue 应用的渲染流程可以概括为:

  1. 模板 (Template)JSX:开发者编写的声明式 UI 代码。
  2. 编译 (Compile):在构建时,Vue 的编译器将模板编译成 render 函数。
  3. 执行 (Execute):运行时,render 函数被执行,返回一个 VNode 树。
  4. 挂载/更新 (Mount/Update):Vue 的渲染器(Renderer)将 VNode 树转换为真实 DOM,并响应数据变化进行高效更新。

render 函数和 VNode 位于流程的第 3 步,是连接声明式代码和真实 DOM 的桥梁。


二、h() 函数:VNode 的创建工厂

h() 函数是创建 VNode 的核心工具。它的名字来源于“hyperscript”,意为“用 JavaScript 创建 HTML 脚本”。

1. 函数签名

ts
function h(
  type: string | Component,      // 节点类型
  props?: object | null,         // 属性/props
  children?: VNodeChildren        // 子节点
): VNode

2. h() 返回了什么?

h() 返回一个 VNode 对象,它是一个轻量级的、纯 JavaScript 的数据结构,用于描述一个 DOM 节点或组件实例。

一个典型的 VNode 对象包含以下关键属性:

js
{
  __v_isVNode: true,        // 私有标志,标识这是一个 VNode
  type: 'div',              // 节点类型:标签名、组件、函数式组件等
  props: { id: 'app' },     // 节点的属性/props
  children: [...],          // 子 VNode 数组或字符串
  key: 'unique-key',        // 用于 diff 算法的唯一标识
  el: null,                 // 指向对应的真实 DOM 元素(挂载后)
  component: null,          // 如果是组件 VNode,指向组件实例
  shapeFlag: 6,             // 形状标志,描述节点类型(如元素、组件、有 children 等)
  patchFlag: 0              // 优化标志,用于编译时静态提升
}

h() 的作用就是根据传入的参数,构造出这样一个结构化的对象。

3. 使用示例

js
import { h } from 'vue';

// 创建一个普通元素 VNode
const vnode1 = h('div', { id: 'container' }, [
  h('h1', 'Hello'),
  h('p', 'World')
]);

// 创建一个组件 VNode
const MyComponent = { /* 组件选项 */ };
const vnode2 = h(MyComponent, { msg: 'Hello' });

三、VNode 如何描述一个组件?

VNode 不仅能描述原生 DOM 元素,还能描述组件。这是 Vue 组件化能力的基础。

1. 组件 VNode 的关键属性

h() 的第一个参数是一个组件(对象或函数)时,生成的 VNode 会以不同方式描述组件:

  • type: 指向组件的定义对象(如 { setup, render, ... })。
  • props: 传递给组件的 props。
  • children: 插槽内容(slot content)。
  • component: 在组件实例创建后,此属性会指向组件的 ComponentInternalInstance

2. 组件实例的创建过程

当渲染器遇到一个组件 VNode 时,会执行以下步骤:

  1. 创建组件实例

    js
    const instance = createComponentInstance(vnode);

    实例包含 setup 状态、propsslots、生命周期钩子等。

  2. 设置组件 VNode 的引用

    js
    vnode.component = instance;
  3. 调用 setup(): 执行组件的 setup 函数,获取渲染上下文。

  4. 执行组件的 render 函数: 调用组件自身的 render 函数,生成组件内部的 VNode 树(即子树)。

  5. 挂载子树: 将组件的 VNode 子树挂载到 DOM 中。

3. 示例:组件的 VNode 结构

js
// 父组件的 render 函数
function render() {
  return h('div', [
    h(MyButton, { text: 'Click me' }) // 组件 VNode
  ]);
}

// MyButton 组件的 render 函数
function render() {
  return h('button', this.text);
}

渲染过程:

  1. 父组件 render 返回一个包含 MyButton 组件 VNode 的树。
  2. 渲染器识别 type 为组件,创建 MyButton 实例。
  3. 执行 MyButtonsetuprender,返回 button 元素的 VNode
  4. button 的真实 DOM 插入页面。

四、render 函数的执行过程

render 函数是组件的“蓝图生成器”。它在以下时机执行:

  • 首次挂载:组件第一次被渲染。
  • 响应式更新:当组件依赖的响应式数据发生变化时。

1. 执行流程

js
function renderComponent(instance) {
  // 1. 收集依赖
  // 当访问响应式数据时,组件实例被作为依赖收集
  const proxyToUse = instance.proxy; // 代理对象,提供 this 访问
  const vnode = instance.render.call(proxyToUse); // 执行 render 函数

  // 2. 返回 VNode
  return vnode;
}

2. 依赖收集的关键

render 函数的执行是响应式的。当它访问 this.count(一个 refreactive 属性)时:

  • 触发 get 拦截器。
  • Vue 将当前组件实例(作为依赖)收集到 count 的依赖列表中。

当下次 count 被修改时,这个依赖会被触发,导致 render 函数重新执行,生成新的 VNode 树。


五、VNode 的优化机制

Vue 3 通过 VNode 上的额外信息实现高效更新:

1. shapeFlag

一个位掩码(bitmask),描述 VNode 的“形状”:

  • 1:普通元素
  • 2:函数式组件
  • 4:有状态组件
  • 8:带有文本子节点
  • 16:带有数组子节点

渲染器根据 shapeFlag 快速判断节点类型,避免冗余检查。

2. patchFlag

由编译器生成的优化标志,标记 VNode 的动态部分:

  • 1:文本内容动态
  • 2:类名动态
  • 4:样式动态
  • 8:全属性动态
  • 16:关键属性动态(如 idvalue

patch(更新)过程中,渲染器可以跳过静态部分,只更新动态部分,极大提升性能。


六、与模板的对比

开发者通常使用模板:

vue
<template>
  <div id="app">
    <h1>{{ title }}</h1>
    <MyButton :text="buttonText" />
  </div>
</template>

Vue 编译器会将其编译为等效的 render 函数:

js
function render() {
  return h('div', { id: 'app' }, [
    h('h1', this.title),
    h(MyButton, { text: this.buttonText })
  ]);
}

模板是语法糖,最终都会变成 h() 调用。JSX 也是类似的语法糖。


七、总结

  • h() 函数:返回一个 VNode 对象,它是描述 UI 的纯 JavaScript 数据结构。
  • VNode:包含 typepropschildrenkey 等属性,既能描述原生元素,也能描述组件。
  • 组件描述:通过 VNodetype 指向组件定义,props 传递数据,children 传递插槽,component 指向实例。
  • render 函数:在挂载和更新时执行,返回 VNode 树。其执行是响应式的,会自动收集依赖。
  • 优化shapeFlagpatchFlag 使 VNode 成为高效 diff 算法的基础。

理解 render 函数和 VNode,就是理解 Vue 如何将声明式代码转化为高效、响应式的 DOM 操作。这是成为高级 Vue 开发者的关键一步。