渲染函数
Vue 建议我们使用模板(即 <template></template>
语法)来构建 Vue 组件的 HTML 部分。然而,我们同样可以直接使用渲染函数来构建组件的 HTML 部分。
Vue 会在构建的时候将我们在组件中编写的模板编译为渲染函数。也正是在这些渲染函数中,Vue 构建了全部节点的一个虚拟体现,也就是虚拟 DOM。
如果你感兴趣,Vue 文档中的 渲染机制 一节中更为详细地介绍了虚拟 DOM 的概念和 Vue 内部的渲染机制。
通过使用渲染函数,我们跳过了 Vue 对我们的模板进行编译的编译步骤,并且可以使用编程形式的 JavaScript 来构建我们的模板。
为什么要用渲染函数
当我们需要更多的灵活性和更高级别的定制化需求时,渲染函数就会开始发挥作用了。而标准的模板无法轻松做到这一点。这可能听上去有些违反直觉,特别是考虑到 Vue 一直在强调其模板的简单和易读。但是,简而言之,你可能在某些情况下更偏爱渲染函数:
当你需要基于复杂的逻辑,动态渲染组件时,在模板中表达这些逻辑将会特别麻烦。
你想要直接使用虚拟 DOM,进行一些高级的操作。
你想要在构建模板或组件时使用 JSX。
除了上述的特殊情况外,Vue 的模板应该依然是构建组件 HTML 的首选方法。然而,对于特殊情况下,了解渲染函数的运行方式是很重要的一件事。因此,我们将在本文中深入研究渲染函数,并探索如何使用渲染函数来构建基本的组件。
渲染函数
假设我们有一个组件,组件的模板部分包含一个 <div>
元素,这个 div 还包裹着一个 <header>
元素。<header>
元素显示了一行文本,文本内容来自 props 中 message
的值。
<template>
<div class="render-card">
<header class="card-header card-header-title">{{ message }}</header>
</div>
</template>
<script setup>
const { message } = defineProps(["message"]);
</script>
接下来我们将使用渲染函数(即 h()
函数),逐步重写这个组件的 HTML 部分。
<script setup>
import { h } from "vue";
const { message } = defineProps(["message"]);
</script>
h
是 hyperscript
的缩写,它是实现虚拟 DOM 的过程中经常使用的术语,意思是 “可以生成 HTML 的 JavaScript”。简而言之, h()
是渲染函数,它允许我们将一个 DOM 节点“虚拟”地表示出来,Vue 会跟踪这个节点的虚拟表示,并随后渲染在页面上。
h()
函数有三个参数:
一个 HTML 标签名,或者是组件定义。
传入元素的 props/attributes(事件监听、class 属性等)。
该节点下的子节点。
我们要构造的父节点 HTML 标签名是 <div>
。我们将 h()
函数的返回值分配给一个常量 render
,并传入一个字符串 'div'
作为第一个参数。
<script setup>
import { h } from "vue";
const { message } = defineProps(["message"]);
const render = () => {
return h("div");
};
</script>
接下来我们想要给父元素 <div>
添加一个 .render-card
CSS 类。我们需要为 h()
函数的第二个参数传入一个对象,其中包含一个 class
属性,属性值是 render-card
:
<script setup>
import { h } from "vue";
const { message } = defineProps(["message"]);
const render = () => {
return h("div", {
class: "render-card",
});
};
</script>
在这个例子中,对于第二个参数的使用仅此而已。我们不会介绍关于第二个参数的更多事情了。但是还有很多种用法来使用第二个参数来定义元素的属性。如果你有兴趣了解,可以查看 Vue 文档。
我们希望父元素 <div>
拥有一个子元素 <header>
。在 h()
函数的第三格参数,我们可以传递一个字符串来渲染文本,或者是传递一个数组来渲染更多的虚拟节点(即更多子元素)。
由于我们需要将另一个渲染的元素 <header>
作为 <div>
的子元素,因此我们要将 h()
函数放在第三个参数的子节点数组中,并传入参数 header
作为第一个参数:
<script setup>
import { h } from "vue";
const { message } = defineProps(["message"]);
const render = () => {
return h(
"div",
{
class: "render-card",
},
[h("header")]
);
};
</script>
子元素 header 也需要 class,所以我们向内部的 h()
函数传入 header
需要的 class:
<script setup>
import { h } from "vue";
const { message } = defineProps(["message"]);
const render = () => {
return h(
"div",
{
class: "render-card",
},
[
h("header", {
class: "card-header card-header-title",
}),
]
);
};
</script>
子元素 header 不包含子元素,只需要将 prop message
显示出来。为了让 header 显示 message
,我们将 message
传入内部的 h()
函数的第三个参数中。
<script setup>
import { h } from "vue";
const { message } = defineProps(["message"]);
const render = () => {
return h(
"div",
{
class: "render-card",
},
[
h(
"header",
{
class: "card-header card-header-title",
},
message
),
]
);
};
</script>
这就完成了!我们要做的最后一件事就是把我们创建的虚拟节点 render
放在组件的模板上。
<template>
<render />
</template>
<script setup>
import { h } from "vue";
/* eslint-disable-next-line no-undef, no-unused-vars */
const { message } = defineProps(["message"]);
/* eslint-disable-next-line no-unused-vars */
const render = () => {
return h(
"div",
{
class: "render-card",
},
[
h(
"header",
{
class: "card-header card-header-title",
},
message
),
]
);
};
</script>
现在我们可以把上面的组件渲染在父组件 App.vue
实例中,然后为 message
prop 传递一个值 "Hello World!"
。
<template>
<RenderComponent message="Hello world!" />
</template>
<script setup>
import RenderComponent from "./components/RenderComponent.vue";
</script>
保存代码后,我们将看到 "Hello World!"
已经显示在了界面上。我们已经成功渲染了组件。
如果你感到困惑,无需担心。尽管渲染函数为我们提供了更多的功能来定制组件的 HTML,但在绝大多数情况下,使用标准的模板语法会来得更容易些。只有在一些复杂的动态渲染或是特殊情况下,人们才会选择渲染函数。
在演练场中查看代码渲染函数和 JSX
我们为了实现上面的例子,过程可能看上去有些痛苦。出现这个感觉的很大部分原因是因为,我们使用了原生的 JavaScript 来编写渲染函数。为了能更简单地编写渲染函数,Vue 为我们提供了可以使用 JSX 编写渲染函数的能力。要使用 JSX 编写渲染函数,需要借助 Babal 插件。
如果你有开发 React 项目的背景,JSX 对你来说可能已经很熟悉了。简而言之,JavaScript XML (也就是常说的 JSX)是一种允许我们使用看起来像 HTML 的 JavaScript 编写渲染函数的扩展。通过 JSX,我们可以在 JavaScript 中编写类似 XML 的代码。
JSX 可以以一种具有更高可读性的方式重新实现我们的渲染逻辑,因为我们可以在渲染函数中直接编写 HTML:
<template>
<render />
</template>
<script setup lang="jsx">
const { message } = defineProps(["message"]);
const render = (
<div class="render-card">
<header class="card-header card-header-title">{message}</header>
</div>
);
</script>
在 JSX 的帮助下,我们的渲染函数看上去没有那么复杂了!需要注意的是,JSX 是一种开发工具,它始终都需要借助 Babel 工具(如 babel-plugin-jsx)来将其编译为标准 JavaScript。create-vue 和 Vue CLI 都有脚手架选项来为项目添加 JSX 支持。
在演练场中查看代码函数式组件
函数式组件,是一种 通过纯函数 方式定义组件的渲染函数。函数式组件是一种没有内部状态的特别的组件。它们很像是单纯的函数,接收 props 作为输入,然后通过返回值输出虚拟节点。
我们使用一个简单的函数(而不是包含选项的对象)来构建一个函数式组件。这个函数本质上是一个渲染函数,主要负责生成组件并输出。
function RenderComponent(props, { slots, emit, attrs }) {
// ...
}
export default RenderComponent;
我们使用 h()
函数创建我们组件的模板,就像之前看到的那样。
import { h } from "vue";
function RenderComponent(props) {
return h(
"div",
{
class: "render-card",
},
[
h(
"header",
{
class: "card-header card-header-title",
},
props.message
),
]
);
}
export default RenderComponent;
此外,我们同样可以使用 JSX 渲染组件的模板,以获得更高的可读性。
function RenderComponent(props) {
return (
<div class="render-card">
<header class="card-header card-header-title">{props.message}</header>
</div>
);
}
export default RenderComponent;
通过这个函数式组件,我们的组件同样可以在 UI 上渲染出 "Hello World!"
。
总结
渲染函数提供了强大的能力让我们可以使用 JavaScript 以编程的方式来构建 Vue 组件的 HTML。渲染函数允许我们创建 Vue 中 DOM 节点的虚拟表示,这有利于 Vue 对其状态进行跟踪和渲染。
虽然渲染函数具有更高的灵活性和可定制性,但与标准模板相比,渲染函数仍然更复杂一些。如果你觉得仍然没懂本文的内容,那完全没有关系。Vue 建议我们尽可能使用标准的模板语法,因为渲染函数更难掌握,也不容易编写。然而,渲染函数在一些特定的场景下十分有用,例如在自定义组件的 HTML 时需要更多的功能以及更高灵活性的时候。