引入

打开 Vue3 的官方文档,它首先会告诉你,Vue 的组件可以按两种不同的风格书写:选项式 API 和组合式 API。文档为我们提供一系列两种风格的代码参考,供我们按照偏好进行选择。

实际上,Vue3 组件可不止两种写法,而是多达十几种!然而,不管是什么写法,它们都是基于同一个底层系统实现的,概念之间也是彼此相通的,只是使用的接口不同。在实际开发中,我们也不会同时使用到那么多种写法,但是这并不意味着我们不需要去了解这些写法!

setup 语法糖

setup 语法糖应该是最常用的写法了。在 Vue3 中,我们想封装一个组件,最习惯的做法还是新建一个 Vue 文件,并将组件代码写在文件中。具体是:页面结构写在 template 中,页面逻辑写在 script 中,页面样式写在 style 中。

总之,我们将与该组件相关的代码都写在一起、放在一个文件中单独维护,在需要该组件的地方引入使用。

这里我们使用了 setup 语法糖,直接在 script 中书写我们的 setup 内部的逻辑。

1
2
3
4
5
6
7
8
9
10
<template>
<div>{{ name }}</div>
</template>

<script setup lang="ts">
import { ref } from "vue";
const name = ref("天气好");
</script>

<style scoped></style>

在 App. vue 中引入并使用:

1
2
3
4
5
6
7
8
9
10
// App.vue
<template>
<User />
</template>

<script setup lang="ts">
import User from "./User.vue";
</script>

<style scoped></style>

注:后续写法尽管形式不同,但它们最终的目的都是导出一个组件,所以对于组件使用方来说(这里是 App. vue),怎么使用这个组件的代码都是不变的,所以将不再重复此代码。

Vue2 选项式写法

Vue2 经典写法

这种写法也是比较经典的。和 setup 语法糖写法类似。我们需要新建一个 vue 文件来存储我们的组件代码,然后在需要使用该组件的地方对其进行引入。区别在于,我们需要在 script 中导出一个 Vue 实例。

这里我们导出的其实是一个普通对象,该对象包含 data、methods 等属性。这个对象的属性都是可选的,即 option,翻译回来即“选项”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<div>{{ name }}</div>
</template>

<script lang="ts">
export default {
data: () => {
return {
name: "天气好",
};
},
};
</script>

<style></style>

defineComponent 辅助函数

尽管我们在 script 语言块中导出的默认对象会被 vue 编译器当成 vue 实例,但不管怎么看,它依旧只是一个 plain object。在定义组件实例方面,vue 提供了一个名为 defineComponent 辅助接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<div>{{ name }}</div>
</template>

<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
data: () => {
return {
name: "天气好",
};
},
});
</script>

<style></style>

尽管这个接口也不能改变我们导出的是一个普通对象的事实,但是它可以为我们的实例提供强大的类型推导。我们可以把它看成是一个返回 vue 实例的工厂函数,让我们的代码看起来更加规范。

Vue3 选项式写法

在 Vue3 中,官方引入了新的选项 setup,这是 Vue3 选项式写法和 Vue2 写法的主要区别。setup 选项的意义在于它允许我们在选项式的写法中引用和使用组合式的 api,比如 onMounted、ref、reactive 等。但对于我们来说,它对于我们有益的地方还是基于它封装起来的 setup 语法糖用起来很方便。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<div>{{ name }}</div>
</template>

<script lang="ts">
export default {
setup() {
return {
name: "天气好",
};
},
};
</script>

<style></style>

使用 defineComponent 时,它能够提示我们 setup 将会接收到什么参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<div>{{ name }}</div>
</template>

<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
setup(prop,context) {
return {
name: "天气好",
};
},
});
</script>

<style></style>

以上写法我们都是在 template 上书写我们的页面结构,这也是最常见的几种写法,下面我们来介绍几种了解 vue 底层必不可少的写法,渲染函数。

手写渲染函数

template 模板语法本质上也可以算是一种语法糖。在 vue 编译器上,template 中的内容最终会被翻译为渲染函数,挂载到 vue 实例的 render 属性上。当需要渲染组件时,vue 就执行一次 render,得到对应的虚拟节点树,最后再转变为真实 dom。

Vue 允许我们脱离 template,直接自己书写渲染函数。位置就在导出实例的 render 选项上:

1
2
3
4
5
6
7
8
9
10
11
<script lang="ts">
import { defineComponent } from "vue";
export default defineComponent({
data: () => ({ name: "天气好" }),
render() {
return this.name;
},
});
</script>

<style></style>

在 template 中,我们使用类似 html 的模板语法来描述我们的视图,在 render 函数中又如何描述呢?vue 提供了两个 api:createVnode 和 h。二者没有区别,h 函数只是 createVnode 的缩写。有了 render 函数,我们就不需要写 template 了。

1
2
3
4
5
6
7
8
9
10
11
<script lang="ts">
import { defineComponent, h } from "vue";
export default defineComponent({
data: () => ({ name: "天气好" }),
render() {
return h("div", this.name);
},
});
</script>

<style></style>

在上面的示例中,我们使用 h 函数生成了一个 vNode,并 return 出去,作为本组件最终在被使用时渲染出来的效果。

在 template 中我们可以使用 v-if、v-for、slot 等模板语法,在 h 函数中这些概念也是支持的,只是形式不同,这方面官方文档有具体的示例。总之,template 模板和 render 选项是可以相互替代的。

setup 返回渲染函数

setup 返回 render 方法

一般来说,在选项式语法中,setup 方法返回一个对象,该对象暴露给 template,供 template 使用,具体参考第三个例子(vue3 选项式写法)。如果我们不使用 template,也就没有返回对象的必要了。

在 Vue3 中,还有另外一种不使用 template 的写法,就是在 setup 方法中返回一个 render 方法。

1
2
3
4
5
6
7
8
9
10
11
<script lang="ts">
import { defineComponent, h, ref } from "vue";
export default defineComponent({
setup() {
const name = ref("天气好");
return () => h("div", name.value);
},
});
</script>

<style></style>

注意:

  1. 在选项式中使用 setup 之后,一般不应该再使用 data、生命周期等在选项式写法中常用的选项,而应该把主要逻辑都写在 setup 中,并适当引入组合式的 api。比如,使用 ref,而不是 data 选项。
  2. ref 自动解包是 template 特有的功能,h 函数是没有这个功能的。在 h 函数中引入 ref,记得理所当然地带上 .value

defineComponent 传入setup

就注意中的第一点,我们可以采用下面这种写法:直接在 defineComponent 中书写 setup 函数(如果再省一点就是 setup 语法糖的写法了)。

1
2
3
4
5
6
7
8
9
<script lang="ts">
import { defineComponent, h, ref } from "vue";
export default defineComponent(() => {
const name = ref("天气好");
return () => h("div", name.value);
});
</script>

<style></style>

以上就是渲染函数的写法,是不是有点感觉了呢,一下子就学会了两个 api!后面会提到的 Jsx 写法其实也应该归为渲染函数写法的一种(只要不是 template,而是用 JavaScript 表达页面结构的,都是渲染函数),但是相对于 h 函数,jsx 并不是纯粹的 js,所以我将它们分成了两类。

Vue & Jsx

在render 中使用 jsx

有了前面两类写法介绍的铺垫,接下来引入 jsx 语法就没有什么难理解的点了。

jsx 在 vue 文件中是这样写的。在 render 渲染函数返回值处书写 jsx 替代 h 函数。书写纯 JavaScript 的 h 函数描述结构还是比较繁冗的,jsx 就是简化了的h 函数写法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script lang="tsx">
import { defineComponent } from "vue";
export default defineComponent({
data() {
return { name: "天气好" };
},
render() {
return (
<>
<div>{this.name}</div>
</>
);
},
});
</script>

<style></style>

在 setup 中使用jsx

jsx 和 setup 配合食用更加。在选项式风格中使用 setup,在 setup 中使用组合式 api,并且返回 jsx 书写的渲染函数。

1
2
3
4
5
6
7
8
9
10
11
<script lang="tsx">
import { defineComponent, ref } from "vue";
export default defineComponent({
setup() {
const name = ref("天气好");
return () => <>{name.value}</>;
},
});
</script>

<style></style>

defineComponent 简写

这个其实就是前面介绍过的 「defineComponent 传入 setup」 函数写法:这里的区别只是使用 jsx 替代了 h 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
<script lang="tsx">
import { defineComponent, ref } from "vue";
export default defineComponent(() => {
const name = ref("天气好");
return () => (
<>
<div>{name.value}</div>
</>
);
});
</script>

<style></style>

自行导出 vNode 对象

我们也可以自己将 render 函数执行一遍,然后将得到的 jsx Element 导出,和上一个示例「defineComponent 简写」是十分相似。但是这段代码的缺点非常致命,它不支持接收外部传递来的属性参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
<script lang="tsx">
import { ref } from "vue";
export default (() => {
const name = ref("天气好");
return () => (
<>
<div>{name.value}</div>
</>
);
})();
</script>

<style></style>

不要使用这种写法。这里会提到这样写,只是因为和后面的「函数式组件(其二)」 写法有关联。本写法与其它写法都不同,其它写法导出的都是 JavaScript 对象或者 jsx 对象,而这里我们则是自己执行了一遍渲染函数并得到了虚拟节点,直接将虚拟节点导出去。既然都已经把虚拟节点创建出来了,那自然无法接收 props。

defineComponent 的第二个参数

如果 defineComponent 的第一个参数是 setup 函数,那么它的第二个参数则可以为组件的定义添加需要的选项,但一般除了补充 props 选项,不会再需要其它选项了(组合式 api 和 setup 的入参可以完全替代其它选项)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script lang="tsx">
import { defineComponent } from "vue";
export default defineComponent(
(props) => {
return () => (
<>
<div>{props.userName}</div>
</>
);
},
{
props: { userName: String },
}
);
</script>

<style></style>

直接在 vue 中使用 jsx

这里 jsx 不再只作为返回值,而是直接被某处使用。它可以是被直接导出,或者用在 template 上。

直接导出 jsx 对象

直接将 jsx 对象导出使用。比前面的写法更简洁,做法就是把 setup 里面的内容提到外面。这里需要注意的是我们导出的是一段直接的 jsx 对象(jsx Element),而不是渲染函数。

1
2
3
4
5
6
7
8
<script lang="tsx">
import { ref } from "vue";
const name = ref("天气好");
const User = <>{name.value}</>;
export default User;
</script>

<style scoped></style>

直接用在 template 上

这种写法可以帮助你在自身的组件内复用一些颗粒度更小的组件,它和 setup 语法糖的写法非常接近,只是 User 变量可以作为标签直接使用。

1
2
3
4
5
6
7
8
9
10
11
<template>
<User />
</template>

<script setup lang="tsx">
import { ref } from "vue";
const name = ref("天气好");
const User = <>{name.value}</>;
</script>

<style></style>

函数式组件(其一)

你还可以将 User 写成函数式组件,在本页面内使用。但它不会将连字符属性转换为小驼峰写法。这和「直接用在 template 上」的内容都是一样的,它们都是为了方便在组件本身复用一些常用的组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
<template>
<User :user-name="name" />
</template>

<script setup lang="tsx">
import { ref } from "vue";
const name = ref("天气好");
const User = (props: { "user-name": string }) => {
return <>{props["user-name"]}</>;
};
</script>

<style></style>

如果你经常使用 tailwind,你可能就会知道什么情况下会出现小颗粒度的可复用标签,比如,一个加了一大堆类名的 div 标签。

独立的 Jsx 文件

以上介绍的所有写法,都是在 .vue 文件中书写的,而且也离不开