Vue2 vs Vue3 的 h 函数:渲染函数完整迁移指南

27 阅读4分钟

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 表达力更强。

核心心智模型始终不变:

h(type, props, children)
// 类型 + 属性 + 子节点 三件套

变的是属性对象的组织方式,以及事件/插槽的传递约定。

━━━

二、Vue 2 的 createElement

基本形态

export default {
  render(createElement) {
    return createElement('div', { class: 'box' }, 'Hello')
  }
}

data 对象字段(重点)

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
})

⚠️ 子节点限制

// Vue 2 ❌ 数字会被忽略
createElement('div', [1, 2])

━━━

三、Vue 3 的 h()

基本形态

import { h } from 'vue'

export default {
  render() {
    return h('div', { class: 'box' }, 'Hello')
  }
}

与 Vue 2 的关键区别

能力Vue 2Vue 3
children 类型字符串 / VNode / 数组+ 数字 / 布尔 / 函数
attrs/props/domProps分字段统一平铺
事件 on / nativeOn分开合并为 onXxx
插槽data.scopedSlots第三个参数 children 对象
多根节点✅ Fragment

props 平铺 + 官方修饰符前缀

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 赋值
h('div', { '.name': 'some-name', '^width': '100' })

━━━

四、事件处理的变化(迁移最容易踩坑)

Vue 3 移除了 nativeOn.native

// 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 拼接:

h('input', {
  onClickCapture() {},
  onKeyupOnce() {}
})

② 其余修饰符 —— 用 withModifiers

import { h, withModifiers } from 'vue'
h('div', { onClick: withModifiers(() => {}, ['self', 'prevent']) })

⚠️ Vue 2 用前缀符号:'!click'(capture)、'~keyup'(once)、'~!mouseover'。迁移时要改写!

━━━

五、作用域插槽

Vue 2(写在 data 里)

createElement(MyComponent, {
  scopedSlots: {
    default: (props) => createElement('span', props.text)
  }
})

Vue 3(写在第三个参数)

import { h } from 'vue'

h(MyComponent, null, {        // ⚠️ 无 props 时传 null,避免被当 props
  default: (props) => h('span', props.text),
  header: () => h('h1', '标题')
})

子组件调用:

// 子组件内
setup(props, { slots }) {
  return () => h('div', null, slots.default({ text: 'hi' }))
}

━━━

六、函数式组件

Vue 2

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)

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 多根节点

import { h, Fragment } from 'vue'
h(Fragment, [h('div', 'a'), h('div', 'b')])

Teleport

import { h, Teleport } from 'vue'
h(Teleport, { to: '#modal' }, () => h('div', '弹窗'))

自定义指令 withDirectives

import { h, withDirectives } from 'vue'
const pin = { mounted() {}, updated() {} }
const vnode = withDirectives(h('div'), [[pin, 200, 'top', { animate: true }]])

v-model 手动展开

h(SomeComponent, {
  modelValue: props.modelValue,
  'onUpdate:modelValue': (v) => emit('update:modelValue', v)
})

━━━

八、template ref 版本差异

// 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 必须唯一(官方强调约束)

// ❌ 同一个 vnode 对象不能复用
const p = h('p', 'hi')
return h('div', [p, p])

// ✅ 用工厂函数
return h('div', Array.from({ length: 20 }).map(() => h('p', 'hi')))

━━━

十、完整差异速查表

概念Vue 2Vue 3
名称createElementh(= createVNode
引入参数传入显式 import
data 结构attrs/props/domProps/on/nativeOn 分字段统一平铺
插槽scopedSlotschildren 对象
函数式组件functional: true普通函数
多根✅ Fragment
性能优化无 patchFlagpatchFlag / shapeFlag

━━━

总结

渲染函数迁移的核心就是三件事:

  1. 🔧 data 拆字段 → props 平铺
  2. 🔧 on/nativeOn → onXxx
  3. 🔧 scopedSlots → 第三个参数 children 对象

记住 h(type, props, children) 三件套,剩下的只是约定变化。

💡 Vue 3.4 起不再隐式注册全局 JSX 命名空间,用 TSX 需在 tsconfig 配 jsxImportSource: "vue",且 Vue 的 JSX ≠ React 的 JSX。


本文对照 Vue 官方文档校验,如有疏漏欢迎评论指正。如果对你有帮助,点赞 👍 收藏 ⭐ 关注 🚀,后续继续分享 Vue3 进阶内容。