🍓 Vue3 里的 h 函数!

2,001 阅读4分钟

大家好,我是前端架构师,关注微信公众号【程序员大卫】:

  • 回复 [面试] :免费领取“前端面试大全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

hcreateVNode 区别

查看 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/…

参考

www.bilibili.com/video/BV166…