Vue2里的h function到底是什么?怎么用?
很多刚接触Vue2的同学,看到代码里突然冒出个h函数,总会忍不住犯嘀咕:这玩意儿到底是干啥的?和咱平时写的模板有啥关系?不用模板直接写h函数能搞出界面不?今天就把Vue2里的h function彻底拆解,从基础概念到实际用法,再到背后的渲染逻辑,一次性讲透,帮你把这块知识掰碎了吸收~
h function是干啥的?
你可以把h函数理解成Vue里“画蓝图”的工具,咱写Vue组件时,不管是用模板()还是写render函数,最终都会变成一堆描述DOM结构的“虚拟DOM”对象,h函数的作用,就是帮你生成这些虚拟DOM。
举个例子,你写了段模板:
<template> <div class="greeting">Hello, {{ name }}</div> </template>
Vue在编译阶段,会把这段模板转换成渲染函数,而渲染函数里核心的逻辑,就是调用h函数生成虚拟DOM,编译后大概长这样(简化版):
function render(h) { return h('div', { class: 'greeting' }, `Hello, ${this.name}`) }
这里的h函数,其实是createElement
的别名(Vue源码里为了简写才叫h),它接收参数,描述“我要创建一个什么样的DOM元素”,然后把这些描述整合成虚拟DOM对象。
那虚拟DOM是啥?你可以把它想成真实DOM的“轻量级副本”——用JS对象记录了标签名、属性、子节点这些信息,Vue的渲染器会拿着这个“蓝图”,去创建或更新真实DOM,所以h函数就是构建“蓝图”的画笔~
h function的基本语法长啥样?
h函数的核心是三个参数:h(tag, data, children)
,每个参数都有明确分工:
- tag:你要创建的标签名(比如
'div'
、'button'
),或者Vue组件(比如MyComponent
); - data:一个对象,用来放标签的属性、事件、样式这些“元信息”;
- children:子节点,可以是字符串(文本节点)、数组(多个子节点,每个子节点也是h函数调用)。
咱一个个拆参数,结合例子看更清楚:
tag参数:选“建材”
如果是原生HTML标签,直接写字符串,比如h('button', ...)
;如果是Vue组件,直接写组件对象,比如import MyCard from './MyCard.vue'
后,用h(MyCard, ...)
。
data参数:给“建材”加配置
data里能塞的东西特别多,常见的有这些:
- 原生HTML属性:用
attrs
字段,比如<a href="xxx">
对应{ attrs: { href: 'xxx' } }
; - 组件的props:用
props
字段,比如子组件需要title
属性,就写{ props: { title: '标题' } }
; - 事件绑定:用
on
字段,比如点击事件<button @click="handleClick">
对应{ on: { click: handleClick } }
; - 样式和类名:
class
可以是字符串、数组、对象;style
是对象,比如{ class: 'btn primary', style: { color: 'red' } }
。
举个完整data的例子:
h('button', { class: ['btn', { 'btn-danger': isDanger }], // 动态类名 style: { fontSize: '16px' }, // 内联样式 attrs: { id: 'submit-btn' }, // 原生属性 on: { click: handleSubmit } // 事件 }, '提交')
children参数:“建材”的子内容
children可以是简单的文本,比如h('p', {}, '这是一段文字')
;也可以是嵌套的h函数(子元素),甚至是数组(多个子元素)。
比如要做个列表:
h('ul', {}, [ h('li', {}, '第一项'), h('li', {}, '第二项'), h('li', {}, '第三项') ])
这就对应模板里的:
<ul> <li>第一项</li> <li>第二项</li> <li>第三项</li> </ul>
h function适合哪些场景?和模板比有啥区别?
很多同学会问:既然有更直观的模板,为啥还要用h函数?这得看场景——模板适合静态结构多、逻辑简单的页面;h函数适合动态性强、需要JS逻辑深度介入的场景。
举几个典型场景:
高度动态的DOM结构
比如你要根据后端返回的配置,动态生成不同的表单组件,配置可能长这样:
const formConfig = [ { type: 'input', label: '用户名', prop: 'username' }, { type: 'password', label: '密码', prop: 'password' }, { type: 'button', label: '提交' } ]
用h函数就能循环这个数组,动态生成对应的表单元素:
render(h) { return h('form', {}, formConfig.map(item => { if (item.type === 'input') { return h('div', {}, [ h('label', {}, item.label), h('input', { attrs: { type: 'text', name: item.prop } }) ]) } // 其他type的处理... })) }
要是用模板写,得写一堆v-if
、v-for
,逻辑绕起来容易乱;但用h函数,在JS里处理数组、判断逻辑,灵活度拉满。
封装复杂组件库
做UI组件库时,很多基础组件(比如Table、Tree)的结构特别复杂,靠模板很难灵活控制,用h函数可以在JS里精确控制每个节点的生成逻辑。
比如写一个动态表格组件,列的数量、每列的渲染方式都由props控制,用h函数循环生成表头、表格行,比模板里嵌套v-for
+插槽要简洁得多。
性能敏感场景(小众但存在)
模板编译后也是渲染函数,但如果是手动写h函数,能更精准地控制虚拟DOM的生成逻辑,避免模板编译时的额外开销(虽然日常开发这点差异可以忽略,但极端场景有用)。
在Vue组件里咋用h function?
最直接的方式是写render函数,Vue组件选项里有个render
选项,它接收一个参数h
,你返回h函数调用的结果就行。
基础用法:替换模板
比如做个简单的问候组件,不用<template>
,纯render函数:
export default { data() { return { name: 'Vue' } }, render(h) { return h('div', { class: 'greeting' }, `Hello, ${this.name}`) } }
这里要注意,render
函数里的this
指向组件实例,所以能直接访问data
、props
这些。
和模板共存?不行,render优先级更高
如果组件里同时写了<template>
和render
函数,Vue会忽略模板,执行render
函数,所以这俩是“互斥”的,选一种方式写结构就行。
结合组件使用
不仅能创建原生HTML标签,还能创建Vue组件,比如有个<MyButton>
组件,接收label
和onClick
属性:
import MyButton from './MyButton.vue' export default { render(h) { return h(MyButton, { props: { label: '自定义按钮' }, on: { click: this.handleClick } }) }, methods: { handleClick() { /* ... */ } } }
这里tag
参数传的是组件对象MyButton
,data
里用props
传值,on
绑定事件,和原生标签的用法逻辑一致。
用h function容易踩的坑,避坑指南!
虽然h函数灵活,但参数多、结构细,稍不注意就出bug,分享几个高频踩坑点:
data里的事件绑定,别漏了on
字段
错误写法:h('button', { click: handleClick }, '点我')
正确写法:h('button', { on: { click: handleClick } }, '点我')
原因:Vue的渲染器需要通过on
字段来识别“这是事件绑定”,直接写click
会被当成普通属性,根本触发不了事件!
组件传值,别搞混props
和attrs
如果是给Vue组件传值,要用props
字段(前提是子组件声明了props
);如果是给原生HTML标签传属性,要用attrs
字段。
比如给<input>
加placeholder
:
// 正确:原生标签用attrs h('input', { attrs: { placeholder: '请输入' } }) // 错误:给原生标签用props会无效 h('input', { props: { placeholder: '请输入' } })
children的数组,每个元素必须是合法虚拟DOM
如果children
是数组,里面每个元素都得是h函数调用的结果(或文本、组件),要是不小心塞了个普通对象,Vue会报错“Invalid VNode type”。
比如循环生成列表时,别忘用h函数:
// 正确 h('ul', {}, items.map(item => h('li', {}, item.name))) // 错误:直接返回对象,不是虚拟DOM h('ul', {}, items.map(item => ({ text: item.name })))
作用域问题,render里的this要小心
如果在render
函数里用箭头函数,this
会丢失组件实例的上下文!
错误写法:
render: (h) => { // 这里this不是组件实例,拿不到data里的name! return h('div', {}, this.name) }
正确写法用普通函数:
render(h) { return h('div', {}, this.name) }
h function背后,Vue渲染原理是啥?
理解h函数,得结合Vue的虚拟DOM+Diff算法这套渲染逻辑,整个流程分三步:
生成虚拟DOM(h函数的活儿)
当组件渲染时,h函数根据tag
、data
、children
生成虚拟DOM对象,这个对象长得大概这样:
{ tag: 'div', data: { class: 'greeting' }, children: 'Hello, Vue', // 还有其他内部属性... }
它是对真实DOM的“抽象描述”,比直接操作真实DOM轻量太多。
渲染器把虚拟DOM变成真实DOM
Vue的渲染器(renderer)会遍历虚拟DOM树,把每个节点转换成真实DOM元素,比如把<div>
变成document.createElement('div')
,然后把属性、事件、子节点都挂上。
数据变化时,Diff算法更新DOM
当组件数据变化(比如name
从'Vue'
变成'React'
),render
函数会重新执行,生成新的虚拟DOM,这时候渲染器会对比新旧虚拟DOM的差异(Diff算法),只更新变化的部分到真实DOM上,而不是整个页面重绘——这也是Vue性能好的关键。
打个比方:你要装修房子(真实DOM),先画设计图(虚拟DOM),第一次装修按设计图全做;之后修改设计(数据变化),工人只改设计图里变化的地方(Diff),不用把房子全拆了重建~
想更顺手?试试和JSX结合!
Vue2里可以用JSX写渲染逻辑,配置好babel插件(比如@vue/babel-plugin-jsx
)后,JSX会被自动编译成h函数调用,这对习惯React开发的同学特别友好,代码更简洁直观。
比如用JSX写刚才的问候组件:
export default { data() { return { name: 'Vue' } }, render() { return ( <div class="greeting"> Hello, {this.name} </div> ) } }
这段JSX编译后,其实就是h('div', { class: 'greeting' },
Hello, ${this.name},用JSX写动态结构时,比如条件渲染、循环,和React的写法几乎一样:
render() { return ( <ul> {this.items.map(item => ( <li key={item.id}>{item.name}</li> ))} </ul> ) }
这样写比纯h函数调用可读性高很多,同时又保留了h函数的灵活性,如果你觉得纯h函数写起来太繁琐,JSX是个很好的折中方案~
h function是Vue渲染的“幕后功臣”
从本质上讲,h function是Vue连接“数据”和“DOM”的桥梁——它把我们写的模板或render
逻辑,转换成虚拟DOM,再驱动真实DOM的更新,虽然平时写模板时感觉不到它的存在,但理解h函数后,你能更深刻地明白Vue的渲染原理,遇到复杂动态组件、性能优化场景时,也能更游刃有余地驾驭代码~
现在再回头看h函数,是不是觉得它既神秘又亲切?下次遇到需要动态生成DOM、封装复杂组件的场景,不妨试试用h函数(或JSX)来实现,感受下它的灵活性~
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。