前言

h 函数 hyperscript (生成 script)用于生成对应的标签
其实所有的 vue 组件都是经过一次 render 之后转化成 h 函数,然后再去对应的渲染 dom 的。那么使用 h 函数渲染组件有什么优势呢?

sfc 结构

vue 是使用了一段叫做 sfc 的结构,类似于一开始学习前端的时候 body script style,现在 vuetemplate script style

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
<div class="example">{{ msg }}</div>
</template>

<script>
export default {
data() {
return {
msg: "Hello world!",
};
},
};
</script>

<style>
.example {
color: red;
}
</style>

<custom1>
This could be e.g. documentation for the component.
</custom1>

而在 template 结构种的 html 代码 最后会被转化为 h 函数,如下

1
h("div", { class: "example" }, msg);

Vue 通过这个 h 函数去真实的渲染 dom,和虚拟节点的 diff 对比等

如何编写 h 函数

h 函数接收的参数是根据使用者传入的参数决定的,下面只介绍重要的几个部分(有十九种,一般不可能都使用上)

  • 参数一 type: 可以写一个标签或者 component 例如
1
2
h("div", "this is a div tag");
h(YourComponent, "do sth.");
  • 可选参数二 props: 用于给标签或者 component 传递 props,一般是对象形式 可以结合 tw 等 css 使用,可以写事件监听器,注意使用 on 前缀
1
2
3
4
5
6
h("div", { class: "h-full w-full text-[grey]" });
h("div", {
onClick() {
console.log("hahahhaha");
},
});
  • 可选参数三 children:字符串|number|对象类型 用于渲染子节点 注意子节点也可以是 h 函数生成的节点,可以模拟 v-if 的操作
1
2
3
4
h("div", "this is a div tag");
h("div", [h("div", "hahahah"), h("span", "???")]);
const isShow = ref<boolean>(false);
h("div", isShow ? h("div", "show") : "noShow");

注意

一开始编写 h 函数的时候,传递了 type 之后 默认是以下可以直接传递 children 的类型,也就是 h 函数的后续参数是根据用于用户传递的值的类型去判断的


这也就是为什么 prop 是对象类型 而 childrenstring | number | array 类型了 因为方便判断

在 ts/js 中编写 component

其实很早之前就有一个疑问,就是 component 是否可以直接在 javascript 文件里面编写,因为有点时候,只是一点点的内容,其实完全不需要写成一个 vue 文件,需求例如:
在表格上面的某一项,默认显示对应的文本,当点击的时候渲染一个 input
然后 blur 或者 change 事件触发的时候 去调用后台接口改变该项的值
如果写成一个 vue 文件 大致上长这样(可能会写错 但大致是这样的)

1
2
3
4
5
6
7
8
<template>
<input v-if='props.text' onChange='xxx' onBlur='xxx'>{{props.text}}</input>
<div v-else>{{props.text}}</div>
</template>
<script>
export
xxx...
</script>

实际上 做的功能很少 但写起来总感觉 没有必要装成一个 vue 文件 因为最后都是渲染成 h 函数 那为什么不直接写 h 函数呢?

defineComponent = 渲染组件 = 渲染一个带状态的 h 函数

我们利用 defineComponent 这个函数 对应渲染组件
其中他有一个状态 props 和 原来的 props 是一样的
还有一个 setup 函数 也是一样的 接收 props 然后返回一个虚拟节点 也就是我们 html 上的内容
这样有了 setup 函数 我们就有了状态的编写 就大于单独编写 h 函数的作用了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
const editableInputComponent = defineComponent({
props: {
val: String,
onUpdateValue: Function as PropType<(v: string) => void>,
},
setup(props) {
const isEdit = ref<boolean>(false);
const inputRef = ref<any>(null);
const inputValue = ref(props.val || "");
function handleOnClick(e: any) {
e.stopPropagation();
isEdit.value = true;
nextTick(() => {
// inputRef.value && inputRef.value?.focus();
inputRef.value?.focus();
});
}
function handleChange() {
props.onUpdateValue && props.onUpdateValue(inputValue.value);
isEdit.value = false;
}
return () => {
return h(
"div",
{
class: "text-[#0256FF] h-[22.39px]",
// style: "min-height: 22px",
onClick: handleOnClick,
},
isEdit.value
? h(NInput, {
ref: inputRef,
value: inputValue.value,
class: "h-full",
onUpdateValue: (v: any) => {
inputValue.value = v;
},
onChange: handleChange,
onBlur: handleChange,
})
: h(NEllipsis, null, {
default: () => props.val,
})
);
};
},
});

结论就是 defineComponent 就是渲染组件 也就是执行一个带了自我状态的 h 函数
defineComponent = 渲染组件 = 渲染一个带状态的 h 函数

PropType

有个小细节 因为 defineComponentprops 不能很明确的定义 接收到的数据类型
所以使用了 PropType 去拓展他的类型 例如下面把 Function 拓展成接收一个参数类型为 string,返回值为空的函数

1
2
3
4
props: {
val: String,
onUpdateValue: Function as PropType<(v: string) => void>,
},

但需要注意的是是 如果多类型定义 是不能使用 PropType

1
2
3
4
props: {
val: [String,Array as PropType<string[]>],//x 错误的
onUpdateValue: Function as PropType<(v: string) => void>,
},

建议改为

1
2
3
4
5
type selectVal = string | string[]
props: {
val: Object as PropType<selectVal>,
onUpdateValue: Function as PropType<(v: string) => void>,
},

或者直接使用

拓展

关于 h 函数 实操的过程中还是会遇到不知道怎么写的情况,比如不知道怎么写自定义指令,不知道怎么写 v-model
可以参考这篇文章:https://www.lanmper.cn/vue/t9645

结语

要用 h 函数封装组件还是直接写组件去封装 各有利弊 h 函数需要更多的学习成本,如果组件库支持 render 函数编写肯定是更高的,比如 Naive 组件库,优势就是完全函数式编程,适用于不太需要 csshtml 的情况。封装 vue 组件的话,就是能够更方便的编写 htmlcss,缺点就是可能会很冗余。
主要记住一句话:高粒度的封装无法适应低粒度的变化。谨慎封装!