大家好,我是前端架构师,关注微信公众号【程序员大卫】:
- 回复 [面试] :免费领取“前端面试大全2025(Vue,React等)”
- 回复 [架构师] :免费领取“前端精品架构师资料”
- 回复 [书] :免费领取“前端精品电子书”
- 回复 [软件] :免费领取“Window和Mac精品安装软件”
前言
h 函数在 Vue 中是一个非常重要的概念,它对于我们理解 Vue 里的渲染机制很关键。当我们在编写 Vue 的 template 模板时,实际上并不是在写 HTML,而是在以一种更直观的方式编写对 h 函数的调用。h 函数的作用是创建虚拟节点(VNode)。
h函数的作用类似于 React 中的createElement
Template 如何转换成 h 函数
假设 main.ts 代码如下:
import { createApp } from "vue";
import App from "./App.vue";
createApp(App).mount("#app");
App.vue 代码如下:
<!-- App.vue -->
<script setup lang="ts">
import Welcome from "./components/Welcome.vue";
</script>
<template>
<div class="box">
<p>Title</p>
<Welcome msg="Hello" :num="999" />
</div>
</template>
那么我们该如何用 h 函数来模拟实现 App.vue 中的内容呢?
h函数的第一个参数是 DOM 的节点类型,或者是一个组件;- 第二个参数是 DOM 的属性,或者组件的 props;
- 第三个参数是子节点。
示例代码如下:
import { createApp, defineComponent, h } from "vue";
import Welcome from "./components/Welcome.vue";
const App = defineComponent({
render: () => {
return h("div", { class: "box" }, [
h("p", "Title"),
h(Welcome, { msg: "Hello", num: 999 }),
]);
},
});
createApp(App).mount("#app");
h 函数的几种用法
1. h 创建 VNode
h 函数可以创建一个 VNode 实例,可以设置样式和绑定事件,再通过 <component :is="VNode"> 的方式将其动态渲染出来。
<!-- App.vue -->
<script setup lang="ts">
import { h } from "vue";
const com = h(
"div",
{
style: { color: "red" },
onClick: () => {
console.log(1111);
},
},
"Hello World",
);
</script>
<template>
<component :is="com" />
</template>
2. h 创建函数组件
2.1 响应式函数组件
下面定义的 Com 是一个组件,而不是一个 VNode,它的写法和 React 中的函数式组件非常相似。
<!-- App.vue -->
<script setup lang="ts">
import { h, ref } from "vue";
const msg = ref("Hello");
const Com = () => h("div", { style: { color: "red" } }, msg.value);
setTimeout(() => {
msg.value = "1111";
}, 1000);
</script>
<template>
<component :is="Com" />
</template>
这里我们在函数调用时访问了 msg.value,而这个函数又是通过 <component :is="Com" /> 这样的方式动态渲染的,它的调用环境是响应式的。
需要注意的是,Com 不能写成下面这种形式:
const Com = h("div", { style: { color: "red" } }, msg.value);
因为在 setup 中它不是一个函数组件,而是一个普通的 VNode,直接在模板中使用时不会触发响应式。而变成函数之后,在模板中调用这个函数,就相当于在模板中使用一个响应式组件(即 effect)。
2.1 接收 props 的函数的组件
由于 Com 是一个组件,我们还可以给它传递参数:
<!-- App.vue -->
<script setup lang="ts">
import { h, type FunctionalComponent } from "vue";
const Com: FunctionalComponent<{ count: number }> = (props) =>
h("div", null, props.count);
</script>
<template>
<Com :count="1"></Com>
</template>
3. h 渲染插槽内容
3.1 渲染默认插槽
我们可以将 Com 理解为一个 setup 函数,所以可以从第 2 个参数里获取到 slots。
<!-- App.vue -->
<script setup lang="ts">
import { h, type FunctionalComponent } from "vue";
const Com: FunctionalComponent = (_props, { slots }) =>
h("div", { style: { color: "red" } }, slots);
</script>
<template>
<Com>
<div>Hello World</div>
</Com>
</template>
3.2 渲染具名插槽
<!-- App.vue -->
<script setup lang="ts">
import { h, type FunctionalComponent } from "vue";
const Com: FunctionalComponent = (_props, { slots }) =>
h("div", { style: { color: "red" } }, [
slots?.default?.(),
"Middle Content",
slots?.header?.(),
]);
</script>
<template>
<Com>
<div>Hello World</div>
<template #header>
<div>header</div>
</template>
</Com>
</template>
最终渲染结果如下:
Hello World
Middle Content
header
3.3 渲染作用域插槽(含插值参数)
注意下面的代码中,<template #header="num"> 不要写成 <template #header="{ num }">,因为 slots.header 传递的是数字,而不是 {num: num.value}。
<script setup lang="ts">
import { h, ref, type FunctionalComponent } from "vue";
const Com: FunctionalComponent = (_props, { slots }) => {
const num = ref(9999);
return h("div", { style: { color: "red" } }, [
slots?.default?.(),
"Middle Content",
slots?.header?.(num.value),
]);
};
</script>
<template>
<Com>
<div>Hello World</div>
<template #header="num">
<div>header{{ num }}</div>
</template>
</Com>
</template>
4. h 渲染组件并传递属性和事件
App.vue 组件:
<!-- App.vue -->
<script setup lang="ts">
import { h, type FunctionalComponent } from "vue";
import HelloWorld from "./components/HelloWorld.vue";
const Com: FunctionalComponent = () => {
return h(HelloWorld, {
msg: "1111",
onFoo: (text) => {
console.log(text);
},
});
};
</script>
<template>
<Com></Com>
</template>
HelloWorld 组件:
<script setup lang="ts">
defineProps<{ msg: string }>();
const emits = defineEmits(["foo"]);
setTimeout(() => {
emits("foo", "HelloWorld");
}, 1000);
</script>
<template>
<div>
{{ msg }}
</div>
</template>
5. h 渲染组件并传递插槽
我们先来看使用 h 渲染组件时,如果不传递任何内容,会是什么效果:
App.vue 组件:
<!-- App.vue -->
<script setup lang="ts">
import { h, type FunctionalComponent } from "vue";
import HelloWorld from "./components/HelloWorld.vue";
const Com: FunctionalComponent = () => {
return h(HelloWorld);
};
</script>
<template>
<Com></Com>
</template>
HelloWorld 组件:
<template>
<div>
<slot>Hello World</slot>
<br />
<slot name="footer" foo="cccc">Footer</slot>
</div>
</template>
最终页面渲染为:
Hello World
Footer
如果我们传入插槽内容,需要注意:此时 h 函数的第三个参数不是 数组,而是一个 对象:
const Com: FunctionalComponent = () => {
return h(HelloWorld, null, {
default: () => h("div", "aaaa"),
footer: ({ foo }: { foo: string }) => h("div", "bbbb " + foo),
});
};
最终渲染结果:
aaaa
bbbb cccc
当然,我们也可以将 <Com> 组件中的插槽内容,传递给 HelloWorld 组件。例如这样写:
<!-- App.vue -->
<script setup lang="ts">
import { h, type FunctionalComponent } from "vue";
import HelloWorld from "./components/HelloWorld.vue";
const Com: FunctionalComponent = (_props, { slots }) => {
return h(HelloWorld, null, {
default: () => slots.default?.(),
footer: ({ foo }: { foo: string }) => h("div", "bbbb " + foo),
});
};
</script>
<template>
<Com>
<div>Com 组件插槽内容</div>
</Com>
</template>
最终渲染为:
Com 组件插槽内容
bbbb cccc
h 和 createVNode 区别
查看 Vue 的源码可以发现,h 函数本质上是对 createVNode 的封装。它的第二个参数名叫 propsOrChildren,意味着这个参数既可以用来传递 props,也可以用来传递 children。
因此,h 函数支持多种调用方式,例如:
h("div", h("div", "11111"));
// 等价于
h("div", null, h("div", "11111"));
// 也等价于
h("div", null, [h("div", "11111")]);
Vue 源码中的实现如下:
export function h(type: any, propsOrChildren?: any, children?: any): VNode {
const l = arguments.length
if (l === 2) {
if (isObject(propsOrChildren) && !isArray(propsOrChildren)) {
// single vnode without props
if (isVNode(propsOrChildren)) {
return createVNode(type, null, [propsOrChildren])
}
// props without children
return createVNode(type, propsOrChildren)
} else {
// omit props
return createVNode(type, null, propsOrChildren)
}
} else {
if (l > 3) {
children = Array.prototype.slice.call(arguments, 2)
} else if (l === 3 && isVNode(children)) {
children = [children]
}
return createVNode(type, propsOrChildren, children)
}
}
官方源码地址: github.com/vuejs/core/…