Vue2 vs Vue3 的 h 函数:渲染函数完整迁移指南
概述
h() 是 hyperscript(超脚本) 的缩写,用来以编程方式创建 VNode(虚拟节点) 。它是 Vue 渲染函数(Render Function)的核心 API,模板和 JSX 最终都会编译成 h() / createElement() 调用。
- 🔹 Vue 2 :叫
createElement,由render函数的参数传入 - 🔹 Vue 3 :叫
h,需要从vue中显式import
📌 本文对照 Vue 官方最新文档(2026-07-09) 逐条校验,文末附官方强调的避坑清单。
━━━
一、为什么需要 h()?
模板写大部分 UI 很香,但遇到高度动态的结构(递归树、虚拟列表、动态组件组合)时,渲染函数的 JS 表达力更强。
核心心智模型始终不变:
scss
h(type, props, children)
// 类型 + 属性 + 子节点 三件套
变的是属性对象的组织方式,以及事件/插槽的传递约定。
━━━
二、Vue 2 的 createElement
基本形态
javascript
export default {
render(createElement) {
return createElement('div', { class: 'box' }, 'Hello')
}
}
data 对象字段(重点)
javascript
createElement('div', {
class: { active: true },
style: { color: 'red' },
attrs: { id: 'foo' }, // 普通 HTML 属性
props: { myProp: 'value' }, // 组件 props
domProps: { innerHTML: '<span>x</span>' }, // DOM 属性
on: { click: this.handleClick }, // 事件(组件 + 原生)
nativeOn: { click: this.handleNativeClick }, // ⚠️ 仅组件,监听根元素原生事件
directives: [{ name: 'show', value: true }],
scopedSlots: { // 作用域插槽
default: (props) => createElement('span', props.text)
},
slot: 'header', // 普通插槽
key: 'unique-key',
ref: 'myRef',
refInFor: true
})
⚠️ 子节点限制
javascript
// Vue 2 ❌ 数字会被忽略
createElement('div', [1, 2])
━━━
三、Vue 3 的 h()
基本形态
javascript
import { h } from 'vue'
export default {
render() {
return h('div', { class: 'box' }, 'Hello')
}
}
与 Vue 2 的关键区别
| 能力 | Vue 2 | Vue 3 |
|---|---|---|
| children 类型 | 字符串 / VNode / 数组 | + 数字 / 布尔 / 函数 |
| attrs/props/domProps | 分字段 | 统一平铺 |
| 事件 on / nativeOn | 分开 | 合并为 onXxx |
| 插槽 | data.scopedSlots | 第三个参数 children 对象 |
| 多根节点 | ❌ | ✅ Fragment |
props 平铺 + 官方修饰符前缀
javascript
import { h } from 'vue'
h('div', {
class: { active: true },
style: { color: 'red' },
id: 'foo', // 普通属性
myProp: 'value', // 组件 props
onClick: () => {}, // 事件统一 onXxx
innerHTML: '<span>x</span>'// DOM 属性
})
💡 官方细节 :Vue 会自动判断该用
setAttribute还是赋值 DOM property。也可手动指定:
.name→ 作为 DOM property 赋值^width→ 作为 HTML attribute 赋值
javascript
h('div', { '.name': 'some-name', '^width': '100' })
━━━
四、事件处理的变化(迁移最容易踩坑)
Vue 3 移除了 nativeOn 和 .native:
javascript
// Vue 2
createElement(MyComponent, {
on: { close: fn },
nativeOn: { click: fn } // 监听根元素原生 click
})
// Vue 3
h(MyComponent, {
onClose: fn, // 组件 emits 的 close
onClick: fn // 未声明 emits 时透传到根元素
})
事件修饰符两种写法
① .passive / .capture / .once ------ camelCase 拼接:
javascript
h('input', {
onClickCapture() {},
onKeyupOnce() {}
})
② 其余修饰符 ------ 用 withModifiers:
javascript
import { h, withModifiers } from 'vue'
h('div', { onClick: withModifiers(() => {}, ['self', 'prevent']) })
⚠️ Vue 2 用前缀符号:
'!click'(capture)、'~keyup'(once)、'~!mouseover'。迁移时要改写!
━━━
五、作用域插槽
Vue 2(写在 data 里)
javascript
createElement(MyComponent, {
scopedSlots: {
default: (props) => createElement('span', props.text)
}
})
Vue 3(写在第三个参数)
javascript
import { h } from 'vue'
h(MyComponent, null, { // ⚠️ 无 props 时传 null,避免被当 props
default: (props) => h('span', props.text),
header: () => h('h1', '标题')
})
子组件调用:
javascript
// 子组件内
setup(props, { slots }) {
return () => h('div', null, slots.default({ text: 'hi' }))
}
━━━
六、函数式组件
Vue 2
javascript
export default {
functional: true,
render(createElement, context) {
return createElement('div', context.children)
}
}
// context 含: props / children / slots / scopedSlots / data / parent / listeners / injections
Vue 3(取消 functional: true)
javascript
import { h } from 'vue'
const FunctionalComp = (props, { slots, attrs, emit }) => {
return h('div', attrs, slots.default?.())
}
FunctionalComp.props = ['msg']
📌 Vue 3 的 context 只含
attrs/emit/slots,无this、无状态、无生命周期,渲染成本更低。
━━━
七、Fragment / Teleport / 自定义指令
Fragment 多根节点
javascript
import { h, Fragment } from 'vue'
h(Fragment, [h('div', 'a'), h('div', 'b')])
Teleport
javascript
import { h, Teleport } from 'vue'
h(Teleport, { to: '#modal' }, () => h('div', '弹窗'))
自定义指令 withDirectives
javascript
import { h, withDirectives } from 'vue'
const pin = { mounted() {}, updated() {} }
const vnode = withDirectives(h('div'), [[pin, 200, 'top', { animate: true }]])
v-model 手动展开
javascript
h(SomeComponent, {
modelValue: props.modelValue,
'onUpdate:modelValue': (v) => emit('update:modelValue', v)
})
━━━
八、template ref 版本差异
javascript
// Vue 3.5+ 推荐 useTemplateRef
import { h, useTemplateRef } from 'vue'
setup() {
const divEl = useTemplateRef('my-div')
return () => h('div', { ref: 'my-div' })
}
// Vue 3.5 之前:直接传 ref() 对象
const divEl = ref()
return () => h('div', { ref: divEl })
━━━
九、VNode 必须唯一(官方强调约束)
javascript
// ❌ 同一个 vnode 对象不能复用
const p = h('p', 'hi')
return h('div', [p, p])
// ✅ 用工厂函数
return h('div', Array.from({ length: 20 }).map(() => h('p', 'hi')))
━━━
十、完整差异速查表
| 概念 | Vue 2 | Vue 3 |
|---|---|---|
| 名称 | createElement |
h(= createVNode) |
| 引入 | 参数传入 | 显式 import |
| data 结构 | attrs/props/domProps/on/nativeOn 分字段 | 统一平铺 |
| 插槽 | scopedSlots |
children 对象 |
| 函数式组件 | functional: true |
普通函数 |
| 多根 | ❌ | ✅ Fragment |
| 性能优化 | 无 patchFlag | patchFlag / shapeFlag |
━━━
总结
渲染函数迁移的核心就是三件事:
- 🔧 data 拆字段 → props 平铺
- 🔧 on/nativeOn → onXxx
- 🔧 scopedSlots → 第三个参数 children 对象
记住 h(type, props, children) 三件套,剩下的只是约定变化。
💡 Vue 3.4 起不再隐式注册全局 JSX 命名空间,用 TSX 需在 tsconfig 配
jsxImportSource: "vue",且 Vue 的 JSX ≠ React 的 JSX。
本文对照 Vue 官方文档校验,如有疏漏欢迎评论指正。如果对你有帮助,点赞 👍 收藏 ⭐ 关注 🚀,后续继续分享 Vue3 进阶内容。