掌握 Vue3 的 h 和 createVNode:构建组件的新方式
在 vue3 中,编写组件的核心方式依然是模板。但有时,我们需要更强大的 JavaScript 编程能力来动态地创建界面。这时,h 函数和 createVNode 函数就变得非常重要了。理解它们,能让你在 Vue 开发中更加灵活。
一、为什么需要它们?从模板到虚拟 dom
Vue 的核心是把模板编译成渲染函数。渲染函数负责生成描述页面结构的虚拟 DOM。虚拟 DOM 是轻量的 JavaScript 对象,它代表了真实的 DOM 结构。Vue 会比较新旧虚拟 DOM 的差异(这个过程叫 diffing),然后只更新真实 DOM 中变化的部分,这样效率更高。
模板的便利与限制:
渲染函数的优势:
渲染函数让你直接用 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 中的变化与优化
对比 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')] ); } }性能优化:静态提升
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', '页脚信息') ]; } }
五、什么时候用?怎么用好?
适用场景:
高度动态的 UI: 需要根据复杂逻辑或数据动态决定渲染哪个组件或元素时。
高阶组件 (HOC): 创建包装其他组件、添加额外功能的组件时,渲染函数非常合适。
需要精细控制渲染: 当模板语法难以表达某些特殊渲染逻辑时。
基于模板的库/工具开发: 底层库经常需要直接操作 VNode。
不推荐场景:
简单静态布局: 直接用模板更清晰、更易读。
已有现成模板组件: 如果模板组件能满足需求,没必要重写成渲染函数。
避免性能陷阱:
关键点:不要在渲染函数内频繁创建静态 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 标签本身如果不变)可以考虑拆分。
与 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 应用时更加得心应手。记住,在需要精细控制渲染过程时,它们是非常强大的工具。
本文内容仅供个人学习/研究/参考使用,不构成任何决策建议或专业指导。分享/转载时请标明原文来源,同时请勿将内容用于商业售卖、虚假宣传等非学习用途哦~感谢您的理解与支持!