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

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

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

相关推荐

vue重新渲染组件(重置或者更新)

当数据通过异步操作后,对之前加载的数据进行变更后,发现数据不生效。A组件或者B组件触发数据更新,C组件数据更新了,但是C组件仍显示上一次数据。

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

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

React高阶组件中使用React.forwardRef的技巧

之前使用React.forwardRef始终无法应用于React高阶组件中,关键点就是React.forwardRef的API中ref必须指向dom元素而不是React组件。codepen实例请划到底部。

Vue使用Props绑定Object并且传参

通过Props 给子组件传变量,变量是对象时,子组件无法在首次打开时获取到传入对象数据,并且在父组件中改变对象的属性,子组件也是无法监听

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

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

React Hook父组件获取子组件的数据/函数

我们知道在react中,常用props实现子组件数据到父组件的传递,但是父组件调用子组件的功能却不常用。文档上说ref其实不是最佳的选择,但是想着偷懒不学redux,在网上找了很多教程,要不就是hook的讲的太少

使用Vue 自定义文件选择器组件

文件选择元素是web上最难看的 input 类型之一。它们在每个浏览器中实现的方式不同,而且通常非常难看。这里有一个解决办法,就是把它封装成一个组件。

element-ui 的隐藏滚动组件el-scrollbar

为什么要用el-scrollbar,大家都知道,模拟一个滚动不难,而且市面上有很多这样的库。我考虑的,首先项目用的框架是Vue,然后用的组件库是Element,Element官网也有很多滚动

vue中prop属性传值解析

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

写一个vue组件库_跟着element学习写组件

组件以插件的形式引入使用,当然,也可以直接在页面引入组件文件,两者按需使用。通过源码可知,vue不会重复安装同一个插件。以第一次安装为准,现在,可以在代码中使用组件啦~

点击更多...

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