Vue 中的 render 函数
在 Vue 中,render 函数是编程式创建虚拟 DOM(VNode) 的核心方式,它替代了模板语法,直接通过 JavaScript 逻辑生成节点结构。模板语法最终也会被 Vue 编译为 render 函数,因此 render 是更底层、更灵活的实现方式。
核心概念
- 作用:动态生成复杂 DOM 结构,处理模板难以覆盖的灵活逻辑(如动态标签、复杂条件/列表渲染)。
- 对比模板 :
- 模板:声明式,简洁易读,适合常规场景;
render:命令式,灵活度高,适合动态/复杂逻辑场景。
- 核心函数 :
h(createElement 的简写),用于创建 VNode,语法:h(tag, data, children)。
一、Vue2 中的 render
Vue2 中 render 是组件选项,接收 createElement(简称 h)作为参数,返回 VNode。
1. 基本语法
js
Vue.component('MyButton', {
render(h) {
// h(标签/组件, 节点属性, 子节点)
return h(
'button', // 1. 标签名/组件
{
// 2. 节点属性(class、style、事件、props 等)
class: 'my-btn',
attrs: { type: 'button' }, // 普通 HTML 属性
on: { click: this.handleClick } // 事件绑定
},
'点击我' // 3. 子节点(字符串/VNode 数组)
);
},
methods: {
handleClick() {
alert('Vue2 Render 点击');
}
}
});
2. 渲染组件
js
import Child from './Child.vue';
export default {
render(h) {
return h(Child, {
props: { msg: 'Hello Vue2' }, // 组件 props
on: { change: this.handleChange } // 组件事件
});
},
methods: {
handleChange(val) {
console.log(val);
}
}
};
二、Vue3 中的 render
Vue3 对 render 做了简化,h 函数需手动导入,且支持组合式 API(setup)中直接返回。
1. 选项式 API 用法
js
import { h } from 'vue'; // 手动导入 h 函数
export default {
render() {
return h(
'div',
{ class: 'container', onClick: () => console.log('click') },
[h('h1', 'Vue3 Render'), h('p', '底层渲染方式')] // 子节点数组
);
}
};
2. 组合式 API(setup)用法
setup 可直接返回 render 函数(或 VNode),是 Vue3 推荐方式:
js
import { h, ref } from 'vue';
export default {
setup(props, { emit, slots }) {
const count = ref(0);
const handleClick = () => {
count.value++;
emit('update', count.value);
};
// 返回 render 函数(响应式数据会自动追踪)
return () => h(
'button',
{ onClick: handleClick },
`点击了 ${count.value} 次`
);
}
};
3. h 函数参数详解(Vue3)
js
h(
// 1. 类型:标签名 | 组件 | 异步组件
'div',
// 2. 可选:props/属性(Vue3 用驼峰式事件名)
{
class: ['foo', { bar: true }], // 类名(数组/对象)
style: { color: 'red' }, // 样式
onClick: () => {}, // 事件(驼峰式)
props: { msg: 'hello' }, // 组件 props
attrs: { id: 'box' }, // 普通 HTML 属性(非 props)
key: 'unique-key' // 列表 key
},
// 3. 可选:子节点(字符串 | VNode 数组 | 插槽)
[
h('span', '子节点1'),
slots.default?.() // 默认插槽
]
);
三、常见场景示例
1. 动态标签/组件
根据参数渲染不同标签(替代模板的动态组件):
js
// Vue3 setup
import { h } from 'vue';
export default {
props: {
tag: { type: String, default: 'div' },
content: { type: String, default: '' }
},
setup(props) {
return () => h(props.tag, null, props.content);
}
};
2. 条件渲染(替代 v-if/v-else)
js
import { h, ref } from 'vue';
export default {
setup() {
const isShow = ref(true);
return () => isShow.value
? h('div', '显示内容')
: h('p', '隐藏时的内容');
}
};
3. 列表渲染(替代 v-for)
js
import { h, ref } from 'vue';
export default {
setup() {
const list = ref(['Vue', 'React', 'Angular']);
// 列表需手动加 key
return () => h('ul', list.value.map(item => h('li', { key: item }, item)));
}
};
4. 插槽处理
js
// Vue3 setup 处理插槽
import { h } from 'vue';
export default {
setup(props, { slots }) {
return () => h('div', [
slots.default?.(), // 默认插槽
slots.scoped?.({ text: '作用域插槽内容' }) // 作用域插槽
]);
}
};
四、JSX 与 render 的关系
Vue 支持 JSX 语法(需配置 @vitejs/plugin-vue-jsx 插件),JSX 最终会被编译为 h 函数调用,写法更接近 HTML:
jsx
import { ref } from 'vue';
export default {
setup() {
const count = ref(0);
return () => (
<div class="container">
<button onClick={() => count.value++}>
点击 {count.value} 次
</button>
{[1,2,3].map(item => <li key={item}>{item}</li>)}
</div>
);
}
};
上述 JSX 等价于:
js
return () => h('div', { class: 'container' }, [
h('button', { onClick: () => count.value++ }, `点击 ${count.value} 次`),
[1,2,3].map(item => h('li', { key: item }, item))
]);
五、注意事项
- 优先级 :组件中同时存在
template和render时,render优先级更高(覆盖模板)。 - 性能优化 :避免在
render函数中每次创建新对象/数组(如h的第二个参数),可通过ref/reactive缓存。 - 事件绑定 :Vue2 用
on: { click: fn },Vue3 用onClick: fn(驼峰式)。 - 列表 key :列表渲染时必须手动设置
key(放在h的第二个参数中),避免复用错误。
总结
| 场景 | 推荐方式 |
|---|---|
| 常规/简单结构 | 模板语法 |
| 动态/复杂逻辑 | render 函数 |
| 想兼顾 HTML 可读性 | JSX(编译为 h) |
render 是 Vue 渲染的底层核心,适合处理模板难以实现的灵活逻辑;日常开发中,模板/JSX 更易维护,仅在必要时使用原生 render 函数。
Vue2 和 Vue3 中 render 函数的对比
Vue2 和 Vue3 中 render 函数的核心目标一致(生成 VNode),但因 Vue3 重构了响应式系统、引入组合式 API,并简化了底层 API 设计,两者在 h 函数使用、参数格式、插槽处理、结合方式 等方面存在显著差异。以下从核心维度拆解区别,并结合示例对比。
一、核心差异总览表
| 对比维度 | Vue2 中的 render 函数 | Vue3 中的 render 函数 |
|---|---|---|
| h 函数(createElement) | 作为 render 函数的第一个参数注入,无需导入 | 需手动从 vue 包导入,render 无默认注入 |
| 书写位置/结合方式 | 仅支持选项式 API(render: function(h) {}),依赖 this 访问实例 |
支持选项式 API(少用)+ 组合式 API(setup 返回 render,推荐),setup 中无 this |
| 事件绑定格式 | 嵌套在 on/nativeOn 对象中(on: { click: fn }) |
直接驼峰式平级绑定(onClick: fn),移除 nativeOn |
| 属性绑定 | props/attrs 需嵌套在对应字段(props: { msg: '' }) |
支持平级简写,attrs 仍保留(非 props 属性) |
| 插槽处理 | 区分 this.$slots(普通插槽)/this.$scopedSlots(作用域插槽) |
统一通过 slots 对象(setup 上下文),无需区分普通/作用域插槽 |
| 函数式组件 | 需声明 functional: true,render 接收 context 第二个参数 |
移除 functional 选项,直接返回 VNode 即可 |
| this 指向 | render 内 this 指向组件实例 |
选项式 API 中 this 仍指向实例;setup 返回的 render 无 this |
| 响应式适配 | 依赖 this 上的响应式数据(Object.defineProperty) |
闭包访问 setup 内的 ref/reactive(Proxy),追踪更高效 |
二、关键差异详细解析
1. h 函数的获取方式(最核心区别)
Vue2 中,h(createElement)是框架自动注入到 render 函数的第一个参数,无需手动导入;
Vue3 中,h 函数被抽离为独立导出的 API,必须手动导入后才能使用,且 render 函数不再接收 h 作为参数。
示例对比
js
// Vue2:h 是 render 的参数,无需导入
Vue.component('MyBtn', {
render(h) { // 框架注入 h
return h('button', { on: { click: this.handleClick } }, '点击');
},
methods: { handleClick() {} }
});
// Vue3:手动导入 h,render 无参数(选项式 API)
import { h } from 'vue';
export default {
render() { // 无注入的 h,需先导入
return h('button', { onClick: this.handleClick }, '点击');
},
methods: { handleClick() {} }
};
// Vue3 推荐:setup 返回 render 函数(无 this)
import { h } from 'vue';
export default {
setup() {
const handleClick = () => {};
return () => h('button', { onClick: handleClick }, '点击'); // 闭包使用 h
}
};
2. 事件/属性绑定格式
Vue2 对事件、属性的绑定需嵌套在特定字段中,Vue3 简化了格式,更贴近原生 JS 写法,同时移除了冗余字段。
(1)事件绑定
- Vue2:普通事件写在
on对象,组件原生事件写在nativeOn对象; - Vue3:所有事件直接用驼峰式 平级绑定(如
onClick),移除nativeOn(组件原生事件无需特殊区分)。
(2)属性绑定
- Vue2:props 需写在
props字段,普通 HTML 属性写在attrs字段; - Vue3:props 可平级简写(或仍放
props字段),attrs保留用于非 props 属性,结构更简洁。
示例对比
js
// Vue2:事件/属性嵌套绑定
render(h) {
return h(
'MyComponent', // 自定义组件
{
props: { msg: 'Vue2' }, // 组件 props
attrs: { id: 'btn' }, // 普通 HTML 属性
on: { change: this.handleChange }, // 组件自定义事件
nativeOn: { click: this.handleNativeClick } // 组件原生点击事件
}
);
}
// Vue3:平级绑定,移除 nativeOn
import { h } from 'vue';
export default {
setup() {
return () => h(
'MyComponent',
{
props: { msg: 'Vue3' }, // 可选平级:msg: 'Vue3'
attrs: { id: 'btn' },
onChange: handleChange, // 自定义事件(驼峰)
onClick: handleNativeClick // 原生事件(无需 nativeOn)
}
);
}
};
3. 插槽处理方式
Vue2 区分「普通插槽」(this.$slots)和「作用域插槽」(this.$scopedSlots),使用繁琐;
Vue3 统一了插槽 API,通过 slots 对象(setup 上下文参数)访问所有插槽,无需区分普通/作用域,且无 this 依赖。
示例对比
js
// Vue2:区分 $slots 和 $scopedSlots
render(h) {
return h('div', [
this.$slots.default(), // 普通默认插槽
this.$scopedSlots.content({ val: 123 }) // 作用域插槽
]);
}
// Vue3:setup 中通过 slots 统一访问
import { h } from 'vue';
export default {
setup(props, { slots }) { // 从上下文获取 slots
return () => h('div', [
slots.default?.(), // 默认插槽(可选链避免报错)
slots.content?.({ val: 123 }) // 作用域插槽(无需区分)
]);
}
};
4. 函数式组件的实现
Vue2 中函数式组件需显式声明 functional: true,且 render 函数接收 context 作为第二个参数;
Vue3 移除了 functional 选项,函数式组件可直接写成「返回 VNode 的函数」,或在 setup 中返回 render 函数,更轻量化。
示例对比
js
// Vue2:函数式组件(需声明 functional)
Vue.component('FunctionalComp', {
functional: true, // 必须声明
render(h, context) { // 第二个参数是上下文(props/slots 等)
return h('div', context.props.msg);
},
props: { msg: String }
});
// Vue3:函数式组件(无需声明 functional)
import { h } from 'vue';
// 方式1:直接导出函数
export const FunctionalComp = (props) => h('div', props.msg);
FunctionalComp.props = { msg: String };
// 方式2:setup 返回 render(更推荐)
export default {
props: { msg: String },
setup(props) {
return () => h('div', props.msg);
}
};
5. 响应式适配与 this 指向
- Vue2:
render函数依赖this访问组件实例上的响应式数据(如this.msg),响应式基于Object.defineProperty,需通过this触发依赖收集; - Vue3:setup 返回的
render函数通过闭包 直接访问 setup 内的 ref/reactive 数据,无this依赖,响应式基于 Proxy,追踪更高效、更灵活。
示例对比
js
// Vue2:依赖 this 访问响应式数据
export default {
data() {
return { count: 0 };
},
render(h) {
return h('button', { on: { click: () => this.count++ } }, `点击 ${this.count} 次`);
}
};
// Vue3:闭包访问响应式数据(无 this)
import { h, ref } from 'vue';
export default {
setup() {
const count = ref(0); // 响应式数据
return () => h(
'button',
{ onClick: () => count.value++ }, // 直接访问 count
`点击 ${count.value} 次`
);
}
};
三、其他细节差异
- 优先级 :两者均为
render优先级高于template,但 Vue3 中 setup 返回的 render 优先级最高; - VNode 结构 :Vue3 的 VNode 优化了内部结构(如移除
data.staticClass等冗余字段),但对外的 h 函数参数兼容大部分写法; - 错误处理 :Vue3 对 render 函数的错误捕获更友好,结合
onErrorCaptured可更精准处理渲染错误; - TS 支持:Vue3 的 render 函数(尤其是 setup 写法)对 TypeScript 类型推导更完善,Vue2 需额外声明类型。
四、总结
Vue3 对 render 函数的改造核心是「轻量化、标准化、适配组合式 API」:
- 移除框架注入的
h函数,改为手动导入,降低耦合; - 简化事件/属性绑定格式,移除冗余字段(如
nativeOn); - 统一插槽 API,无需区分普通/作用域插槽;
- 结合 setup 实现无
this的写法,适配 Proxy 响应式系统; - 废弃
functional选项,函数式组件更简洁。
日常开发中,Vue2 仍以选项式 API 的 render 为主,Vue3 推荐使用「setup 返回 render/JSX」的方式,仅在需要极致灵活度时使用原生 render 函数,模板/JSX 仍是更易维护的选择。