一、定义
我们编写的代码转化为真正的dom时,首先会先转换为VNode,然后多个Vnode进行结合起来转化为VDOM,最后VDOM才渲染成真实的DOM。在 Vue.js 中,h 函数是 createElement 的别名,它是 Vue 用来创建虚拟 DOM 节点(vnode)的一个函数。这个函数在编写渲染函数(render functions)时非常有用。通过使用 h 函数,你可以更加灵活地控制应用的输出结构,而不是仅仅依赖于模板。
大部分情况下,我们都是用的模板语法开发,例如:
<template>
<div>
...
</div>
</template>
<script setup lang="ts">
...
</script>
<style lang="less" scoped>
...
</style>
但是,某些开发场景,我们需要借助JavaScript 完全的编程能力,所以我们就要用渲染函数来进行开发,也就是我们本文中所谈到的h函数
二、h函数的类型
// 完整参数签名
function h(
type: string | Component,
props?: object | null,
children?: Children | Slot | Slots
): VNode
// 省略 props
function h(type: string | Component, children?: Children | Slot): VNode
type Children = string | number | boolean | VNode | null | Children[]
type Slot = () => Children
type Slots = { [name: string]: Slot }
- 第一个参数type,必填,既可以是一个字符串 (用于原生元素) 也可以是一个 Vue 组件定义。
- 第二个参数props,非必填,是要传递的 prop。一个对象,包括创建节点的属性,例如 id、class、style等,节点的事件监听也是通过 props 参数进行传递,并且以 on 开头,以 onXxx 的格式进行书写,如 onInput、onClick 等。不写的话推荐使用 null占位
- 第三个参数children是子节点,也可以是数组。
当创建一个组件的 vnode 时,子节点必须以插槽函数进行传递。如果组件只有默认槽,可以使用单个插槽函数进行传递。否则,必须以插槽函数的对象形式来传递。为了方便阅读,当子节点不是插槽对象时,可以省略 prop 参数。
三、h函数创建原生元素
import { h } from 'vue'
// 除了 type 外,其他参数都是可选的
h('div')
h('div', { id: 'foo' })
// attribute 和 property 都可以用于 prop
// Vue 会自动选择正确的方式来分配它
h('div', { class: 'bar', innerHTML: 'hello' })
// class 与 style 可以像在模板中一样
// 用数组或对象的形式书写
h('div', { class: [foo, { bar }], style: { color: 'red' } })
// 事件监听器应以 onXxx 的形式书写
h('div', { onClick: () => {} })
// children 可以是一个字符串
h('div', { id: 'foo' }, 'hello')
// 没有 prop 时可以省略不写
h('div', 'hello')
h('div', [h('span', 'hello')])
// children 数组可以同时包含 vnode 和字符串
h('div', ['hello', h('span', 'hello')])
四、h函数创建组件:
import Foo from './Foo.vue'
// 传递 prop
h(Foo, {
// 等价于 some-prop="hello"
someProp: 'hello',
// 等价于 @update="() => {}"
onUpdate: () => {}
})
// 传递单个默认插槽
h(Foo, () => 'default slot')
// 传递具名插槽
// 注意,需要使用 `null` 来避免
// 插槽对象被当作是 prop
h(MyComponent, null, {
default: () => 'default slot',
foo: () => h('div', 'foo'),
bar: () => [h('span', 'one'), h('span', 'two')]
})
五、h函数的基础使用
1.setup版本
<script>
import { h } from 'vue'
export default {
setup() {
return () => h('h1', { class: 'title' }, 'Hello World')
}
}
</script>
<style lang="less" scoped>
.title {
color: red;
}
</style>
2.render函数版本
<script>
import { h } from 'vue'
export default {
render() {
return h('h1', { class: 'title' }, 'Hello World')
}
}
</script>
<style lang="less" scoped>
.title {
color: red;
}
</style>
效果同上。
六、h函数的实际应用
1.h函函数的v-if和v-show、v-on
<script>
import { ref, h } from 'vue'
import { ElButton } from 'element-plus'
export default {
setup() {
const isShow = ref(true)
return () => h('div', [
h(ElButton, { type: 'primary', onClick: () => isShow.value = !isShow.value }, () => isShow.value ? '隐藏' : '显示'),
h('div', { class: { none: !isShow.value } }, '君不见黄河之水天上来(类似v-show)'),
isShow.value ? h('div', '奔流到海不复回(类似v-if)') : null
])
}
}
</script>
<style lang="less" scoped>
.none {
display: none;
}
</style>
效果图:
2.h函数的v-for,v-on传参
<script>
import { ref, h } from 'vue'
import { ElMessage } from 'element-plus'
export default {
setup() {
const list = [
{ name: '吕布', age: 36, gender: '男' },
{ name: '貂蝉', age: 28, gender: '女' },
{ name: '曹操', age: 40, gender: '男' },
{ name: '刘备', age: 35, gender: '男' },
{ name: '孙权', age: 30, gender: '男' },
];
const handleClick = (item) => {
ElMessage({
message:`您点击的是${item.name}`,
type: 'success'
})
};
return () => h('div', list.map(
(item) => h('div', { onClick: () => handleClick(item) }, `${item.name}今年${item.age}岁,${item.gender}`)
))
}
}
</script>
效果图:
3.h函数的插槽渲染
子组件:
<script>
import { h } from 'vue'
export default {
setup(props, { slots }) {
return () => h('div', [
h('div', '默认插槽开始'),
h('div', slots.default ? slots.default() : null),
h('div', '默认插槽结束'),
h('hr'),
h('div', '具名插槽special开始'),
h('div', slots.special ? slots.special() : null),
h('div', '具名插槽special结束'),
])
}
}
</script>
父组件template版本:
<template>
<hChildren>
<template #default>
<div>父传子的默认插槽内容1</div>
<div>父传子的默认插槽内容2</div>
</template>
<template #special>
<div>父传子的具名插槽special内容1</div>
<div>父传子的具名插槽special内容2</div>
</template>
</hChildren>
</template>
<script setup>
import hChildren from "./hChildren.vue";
</script>
效果图:
父组件h函数版本:
<script>
import { h } from "vue";
import hChildren from "./hChildren.vue";
export default {
setup() {
return () => h(hChildren, null, {
default: [
h('div', '父传子的默认插槽内容1'),
h('div', '父传子的默认插槽内容2')
],
special: h('div', '父传子的具名插槽special内容1')
})
}
}
</script>
效果图:
七、优势
看到这里,有些熟悉react的小伙伴会不会觉得,h函数和jsx还是有些像的。事实上,vue也是支持jsx的。那么相比jsx,h函数肯定是有属于自己的独特优势的。
h 函数的优势包括:
- 更接近 Vue.js 的核心:h 函数是 Vue.js 的核心 API 之一,它直接与 Vue.js 的虚拟 DOM 和组件系统交互。这意味着使用 h 函数可以更直接地访问和操作 Vue.js 的内部机制。
- 更灵活:h 函数可以用于创建任何类型的 VNode,包括自定义组件和渲染器。这使得 h 函数在创建复杂的虚拟 DOM 结构时更加灵活。
- 更简洁:h 函数的语法比 JSX 更简洁。例如,h 函数不需要使用大括号来包裹 JavaScript 表达式,这使得代码更易读。
- 更高效:h 函数在创建 VNode 时可以更高效地利用 Vue.js 的内部优化。例如,h 函数可以更直接地访问 Vue.js 的内部缓存,从而减少不必要的计算。
然而,h 函数也有一些缺点:
- 学习曲线:h 函数的语法比 JSX 更复杂,对于初学者来说可能需要一些时间来学习和适应。
- 可读性:虽然 h 函数的语法比 JSX 更简洁,但对于一些复杂的虚拟 DOM 结构,h 函数的代码可能仍然难以阅读和理解。
总的来说,h 函数和 JSX 都有各自的优点和缺点,选择使用哪一个取决于你的具体需求和偏好。如果你更熟悉 JavaScript,并且希望更直接地访问和操作 Vue.js 的内部机制,那么 h 函数可能是一个更好的选择。如果你更熟悉 JSX,并且希望更简洁的语法和更好的可读性,那么 JSX 可能是一个更好的选择。
在一些第三方的组件库里,会经常提供一些钩子函数来用于虚拟dom的重新构建与定制,诸如:element、antdesign-vue等都有类似的钩子函数。所以h函数我们就算不常用,但是也是必须要会的,还不熟悉h函数的小伙伴,抓紧行动起来,按照上线的例子亲自动手,尝试一下吧~