掌握 Vue3 的 h 和 createVNode:构建组件的新方式

更新日期: 2025-07-17阅读: 59标签: 组件

vue3 中,编写组件的核心方式依然是模板。但有时,我们需要更强大的 JavaScript 编程能力来动态地创建界面。这时,h 函数和 createVNode 函数就变得非常重要了。理解它们,能让你在 Vue 开发中更加灵活。


一、为什么需要它们?从模板到虚拟 dom

Vue 的核心是把模板编译成渲染函数。渲染函数负责生成描述页面结构的虚拟 DOM。虚拟 DOM 是轻量的 JavaScript 对象,它代表了真实的 DOM 结构。Vue 会比较新旧虚拟 DOM 的差异(这个过程叫 diffing),然后只更新真实 DOM 中变化的部分,这样效率更高。

  1. 模板的便利与限制:

    • 优点: 模板语法简单直观,特别适合描述大部分静态或简单动态的 UI 结构。它让 html 的结构清晰可见。

    • 局限:

      • 动态组件受限: 需要特殊的 <component :is="..."> 语法来切换组件。

      • JavaScript 能力不足: 模板内能写的 JavaScript 表达式有限,复杂的逻辑往往需要拆分成计算属性或方法,然后在模板里调用。

      • 逻辑分散: 复杂的 UI 逻辑有时不得不分开写在模板(结构)和 <script> 标签(行为)里,不够集中。

  2. 渲染函数的优势:

    • 渲染函数让你直接用 JavaScript(TypeScript)来构建虚拟 DOM。

    • 更强的编程能力: 你可以使用 JavaScript 的全部特性(条件语句、循环、函数调用、变量等)来灵活地创建和组合 VNode(虚拟节点)。

    • 逻辑集中: 复杂的渲染逻辑可以完全写在一个函数里,更容易管理和复用。

    • 动态性更强: 根据运行时数据动态决定渲染什么组件或元素变得非常自然。


二、h 与 createVNode:它们是什么关系?

  • createVNode: 这是 Vue 内部创建虚拟节点(VNode)的核心函数。它接收必要的参数(元素类型、属性、子节点等),构造并返回一个 VNode 对象。

  • h: 你可以把 h 看作是 createVNode 的一个“用户友好版”或者“语法糖”。它内部最终也是调用 createVNode。

    • 关键区别: h 函数提供了更灵活的参数处理。它允许你用不同的方式传递参数(比如省略 props 直接传 children),让代码写起来更方便。createVNode 的参数要求通常更严格。

简单来说:h 最终调用 createVNode 来干活,但 h 写起来更顺手。 在 Vue 组件中,我们几乎总是使用 h。

// 源码简化示意 (vue-next/runtime-core/src/vnode.ts)
export function createVNode(type, props, children) {
  // ... 内部创建 VNode 的逻辑
}

export function h(type, propsOrChildren, children) {
  // h 会聪明地判断第二个参数是 props 还是 children
  if (arguments.length === 2) {
    if (isObject(propsOrChildren) && !isArray(propsOrChildren)) {
      // 第二个参数是 props (对象) 且没有 children
      return createVNode(type, propsOrChildren);
    } else {
      // 第二个参数是 children (数组或字符串等)
      return createVNode(type, null, propsOrChildren);
    }
  } else {
    // 有 type, props, children 三个参数
    return createVNode(type, propsOrChildren, children);
  }
}


三、如何使用 h 函数?(带实例)

h 函数的基本结构是:h(type, props, children)

  • type: 可以是 HTML 标签名 (如 'div'), 也可以是导入的 Vue 组件对象。

  • props: 一个对象,包含要绑定的属性、类、样式、事件监听器等。

  • children: 子节点。可以是字符串(文本)、单个 VNode、或者由 VNode 组成的数组。

实例 1:创建基础元素 (按钮)

import { h } from 'vue';

const button = h(
  'button', // 类型:HTML 按钮
  {
    class: ['btn', 'btn-primary'], // css 类
    onClick: () => console.log('按钮被点了'), // 点击事件
    style: { fontWeight: 'bold' }, // 行内样式
    '>: 'submit-btn' // 自定义属性
  },
  '保存' // 子节点:按钮文字
);

这相当于模板:

<button class="btn btn-primary" style="font-weight: bold;" @click="handleClick" >="submit-btn">
  保存
</button>

实例 2:创建组件

import { h } from 'vue';
import CustomInput from './CustomInput.vue'; // 导入组件

const inputField = h(
  CustomInput, // 类型:导入的组件
  {
    modelValue: '初始值', // 传入 prop (对应 v-model 的值)
    'onUpdate:modelValue': (newValue) => { // 监听更新事件 (对应 v-model 的更新)
      console.log('新值:', newValue);
      // 通常这里会把 newValue 同步到你的状态
    },
    disabled: false // 另一个 prop
  }
);

这相当于模板:

<CustomInput v-model="yourValue" :disabled="false" />

实例 3:创建动态列表

import { h, ref } from 'vue';

const todos = ref([ // 响应式待办列表
  { id: 1, text: '学 Vue', done: true },
  { id: 2, text: '写代码', done: false },
]);

const todoList = h(
  'ul', // 类型:无序列表
  { id: 'my-todo-list' }, // 属性:ID
  // 子节点:根据 todos 数组动态生成 li
  todos.value.map(todo =>
    h(
      'li', // 每个待办项是 li
      {
        key: todo.id, // 重要!为动态列表项提供唯一 key
        class: { 'line-through': todo.done } // 动态类:完成时加删除线
      },
      todo.text // li 的内容是待办文本
    )
  )
);

这相当于模板:

<ul id="my-todo-list">
  <li v-for="todo in todos" :key="todo.id" :class="{ 'line-through': todo.done }">
    {{ todo.text }}
  </li>
</ul>


四、Vue3 中的变化与优化

  1. 对比 Vue2 的 render 函数:

    • 导入方式: Vue3 需要显式从 'vue' 导入 h。

    • api 风格: 在 Vue3 的 Composition API setup() 函数中,我们直接返回一个渲染函数。

    • Props 传递: Vue3 中属性和事件监听器都直接平铺在 props 对象里。Vue2 则需要区分普通属性 (attrs) 和 DOM 属性 (props),事件监听器也放在 on 对象里。

    // Vue2 Options API (对比)
    export default {
      render(h) {
        return h('div', {
          attrs: { id: 'box' }, // 属性在 attrs 里
          on: { click: handleClick } // 事件在 on 里
        }, [
          h('span', 'Hello Vue2')
        ]);
      }
    }
    
    // Vue3 Composition API
    import { h } from 'vue';
    export default {
      setup() {
        const handleClick = () => console.log('Clicked');
        return () => h(
          'div',
          {
            id: 'box', // 属性直接写
            onClick: handleClick  // 事件直接写
          },
          [h('span', 'Hello Vue3')]
        );
      }
    }
  2. 性能优化:静态提升
    Vue3 的编译器非常智能。如果一个节点完全是静态的(不依赖任何响应式数据),编译器会在编译阶段把它提升出去,只创建一次 VNode,然后在每次渲染时复用。这在渲染函数中也能体现:

    import { h, ref } from 'vue';
    
    // 静态节点:在组件外部创建一次,多次渲染复用
    const staticHeader = h('header', { class: 'app-header' }, [
      h('h1', '我的应用'),
      h('p', '欢迎光临')
    ]);
    
    export default {
      setup() {
        const count = ref(0);
        return () => [
          staticHeader, // 复用静态 VNode
          h('main', null, [
            h('p', `当前计数: ${count.value}`) // 动态节点
          ]),
          h('footer', '页脚信息')
        ];
      }
    }


五、什么时候用?怎么用好?

  1. 适用场景:

    • 高度动态的 UI: 需要根据复杂逻辑或数据动态决定渲染哪个组件或元素时。

    • 高阶组件 (HOC): 创建包装其他组件、添加额外功能的组件时,渲染函数非常合适。

    • 需要精细控制渲染: 当模板语法难以表达某些特殊渲染逻辑时。

    • 基于模板的库/工具开发: 底层库经常需要直接操作 VNode。

  2. 不推荐场景:

    • 简单静态布局: 直接用模板更清晰、更易读。

    • 已有现成模板组件: 如果模板组件能满足需求,没必要重写成渲染函数。

  3. 避免性能陷阱:

    • 关键点:不要在渲染函数内频繁创建静态 VNode。 这会导致每次渲染都生成新对象,增加 diff 成本。

    • 错误做法:

      function BadList() {
        return h('ul', null,
          ['苹果', '香蕉', '橘子'].map(item => h('li', item)) // 每次渲染都创建新数组和新 VNodes
        );
      }
    • 正确优化:缓存静态部分

      // 在组件外部或 setup 顶层创建一次 (如果数据是静态的)
      const fruitItems = ['苹果', '香蕉', '橘子'].map(item => h('li', item));
      
      function GoodList() {
        return h('ul', null, fruitItems); // 复用静态 VNode 数组
      }
      • 如果数据是响应式的,动态部分(如列表项内容)仍然需要在渲染函数内部根据数据生成,但静态的结构(如包裹的 ul 和 li 标签本身如果不变)可以考虑拆分。

  4. 与 JSX 配合:
    如果你觉得 h 函数写起来嵌套太多,Vue3 也支持 JSX。JSX 提供了一种更接近 HTML 的语法来写渲染函数。

    • 配置 JSX (以 Vite 为例):

      // vite.config.js
      import vue from '@vitejs/plugin-vue';
      
      export default {
        plugins: [
          vue({
            jsx: true // 启用 Vue JSX 支持
          })
        ]
      };
    • 使用 JSX:

      import { ref, defineComponent } from 'vue';
      
      export default defineComponent({
        setup() {
          const count = ref(0);
          return () => (
            <div class="counter">
              <button onClick={() => count.value++}>
                点了 {count.value} 次
              </button>
              <ul>
                {Array.from({ length: count.value }).map((_, i) => (
                  <li key={i}>第 {i + 1} 项</li>
                ))}
              </ul>
            </div>
          );
        }
      });

      JSX 最终会被编译成 h 函数调用。选择 h 还是 JSX 主要看个人或团队偏好。


总结:

Vue3 的 h 函数(和底层的 createVNode)为你提供了直接操作虚拟 DOM 的能力。虽然模板在大多数情况下是首选,但在需要极致动态性和灵活性的场景下,掌握渲染函数(或 JSX)是成为 Vue 高级开发者的关键一步。理解其工作原理、适用场景和性能优化点,能让你在构建复杂 Vue 应用时更加得心应手。记住,在需要精细控制渲染过程时,它们是非常强大的工具。

本文内容仅供个人学习/研究/参考使用,不构成任何决策建议或专业指导。分享/转载时请标明原文来源,同时请勿将内容用于商业售卖、虚假宣传等非学习用途哦~感谢您的理解与支持!

链接: https://fly63.com/article/detial/12870

Vuetify基于vue2.0,为移动而生的组件框架

Vuetify 支持SSR(服务端渲染),SPA(单页应用程序),PWA(渐进式Web应用程序)和标准HTML页面。 Vuetify是一个渐进式的框架,试图推动前端开发发展到一个新的水平。

Vue中插槽的作用_Vue组件插槽的使用以及调用组件内的方法

通过给组件传递参数, 可以让组件变得更加可扩展, 组件内使用props接收参数,slot的使用就像它的名字一样, 在组件内定义一块空间。在组件外, 我们可以往插槽里填入任何元素。slot-scope的作用就是把组件内的数据带出来

react 函数子组件(Function ad Child Component)

函数子组件(FaCC )与高阶组件做的事情很相似, 都是对原来的组件进行了加强,类似装饰者。FaCC,利用了react中children可以是任何元素,包括函数的特性,那么到底是如何进行增强呢?

Vue和React组件之间的传值方式

在现代的三大框架中,其中两个Vue和React框架,组件间传值方式有哪些?组件间的传值是灵活的,可以有多种途径,父子组件同样可以使用EventBus,Vuex或者Redux

vue.js自定义组件directives

自定义指令:以v开头,如:v-mybind。bind的作用是定义一个在绑定时执行一次的初始化动作,观察bind函数,它将指令绑定的DOM作为一个参数,在函数体中,直接操作DOM节点为input赋值。

vue中prop属性传值解析

prop的定义:在没有状态管理机制的时候,prop属性是组件之间主要的通信方式,prop属性其实是一个对象,在这个对象里可以定义一些数据,而这些数据可以通过父组件传递给子组件。 prop属性中可以定义属性的类型,也可以定义属性的初始值。

Web组件简介

Web组件由三个独立的技术组成:自定义元素。很简单,这些是完全有效的HTML元素,包含使用一组JavaScript API制作的自定义模板,行为和标记名称(例如,<one-dialog>)。

web组件调用其他web资源

web组件可以直接或间接的调用其他web资源。一个web组件通过内嵌返回客户端内容的另一个web资源的url来间接调用其他web资源。在执行时,一个web资源通过包含另一个资源的内容或者转发请求到另一个资源直接调用。

vue中如何实现的自定义按钮

在实际开发项目中,有时我们会用到自定义按钮;因为一个项目中,众多的页面,为了统一风格,我们会重复用到很多相同或相似的按钮,这时候,自定义按钮组件就派上了大用场,我们把定义好的按钮组件导出,在全局引用,就可以在其他组件随意使用啦,这样可以大幅度的提高我们的工作效率。

Vue子组件调用父组件的方法

Vue中子组件调用父组件的方法,这里有三种方法提供参考,第一种方法是直接在子组件中通过this.$parent.event来调用父组件的方法,第二种方法是在子组件里用$emit向父组件触发一个事件,父组件监听这个事件就行了。

点击更多...

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!