使用装饰器和class关键字编写Vue2.x组件
您好,我是 沧沧凉凉 ,是一名前端开发者,目前在 掘金 、 知乎 以及 个人博客 上同步发表一些学习前端时遇到的趣事和知识,欢迎关注。
相信目前在前端行业中,大部分项目依然是使用的Vue2.x来进行编写,除了很多人不愿意跳出自己的舒适圈去学习新的东西之外,还有一个原因就是Vue2.x比Vue3.x的第三方库丰富太多,因为Vue3 Composition API的关系,导致很多支持Vue2.x的第三方库与Vue3.x都不兼容。
当然这并不是本篇文章要讲的重点,本篇文章的重点其实是
vue-property-decorator
这个装饰器库提供的装饰器。
之所以要使用装饰器的原因是因为 装饰器可以极大程度简化Vue组件中各种状态的声明 ,并且Mixins的引用会变得更加明确(但是通常不推荐在一个项目中大量使用Mixins,因为会大大降低代码的可读性。)
vue-property-decorator
一共有提供以下几种装饰器:
- @Prop
- @PropSync
- @Model
- @ModelSync
- @Watch
- @Provide
- @Inject
- @ProvideReactive
- @InjectReactive
- @Emit
- @Ref
- @VModel
- @Component
需要值得注意的是,装饰器并不仅仅可以用在ts上面,在js上面一样可以使用! 下面就来看一下这些装饰器的魅力吧:
1. @Component/Mixins
使用class关键字来创建组件的基本方法:
import { Component, Vue, VModel, Mixins } from "vue-property-decorator";
@Component
export default class C extends Vue {}
// Mixins 括号中引入混入的文件
@Component
export default class C extends Mixins() {}
这样就可以创建一个Vue组件,同时 如果你使用了TypeScript,那么你会得到更好的类型推断。
2. @Prop/@Emit
在本库中最常用的两个装饰器,分别对应
Prop
和
this.$emit
。
2.1 @Prop
用法:
@Component
export default class YourComponent extends Vue {
// 下面3个是官方给的用法
@Prop(Number) readonly propA: number | undefined
@Prop({ default: 'default value' }) readonly propB!: string
@Prop([String, Boolean]) readonly propC: string | boolean | undefined
相当于:
export default {
props: {
propA: {
type: Number,
propB: {
default: 'default value',
propC: {
type: [String, Boolean],
我个人比较喜欢使用下面这种写法:
@Component
export default class YourComponent extends Vue {
@Prop({ type: Number, required: true, default: 0 }) readonly propA:
| number
| undefined;
即
@Prop()
括号里面接各种参数。
2.2 @Emit
一般来讲,要更改Prop的值则需要通过
this.$emit("xxx" , value)
这种写法,例如:
change()
: void {
this.$emit("change", 5);
而使用了装饰器后可以这样写:
@Emit()
change(): number {
return 5;
其中return的值就是
$emit
所要传递的值,
如果
@Emit()
不带参数,则默认将函数名作为
$emit
所要触发的名称
,如果带了参数:例如
@Emit("a")
,则相当于
this.$emit("a", 5);
。
同时
@Emit
装饰器会自动将camelCase(驼峰命名)命名转换为kebab-case。
3. @PropSync
因为在Vue2.x里只能拥有一个
v-model
,所以可以使用
.sync
关键字来创建多个类似于
v-model
的双向绑定。
// 父组件通过:name.sync关键字
<B :name.sync="name" />
// 子组件
@PropSync("name") syncName!: string;
@Emit("update:name")
input(): string {
return "张三";
对于
.sync
关键字,可以看
官方的文章
。
4. @VModel
快速创建一个
v-model
:
import { Component, Vue, VModel } from "vue-property-decorator";
@Component
export default class C extends Vue {
@VModel({ type: String }) name!: string;
等同于:
export default {
props: {
value: {
type: String,
computed: {
name: {
get() {
return this.value
set(value) {
this.$emit('input', value)
就是实现一个
v-model
双向绑定,只要在子组件中调用
this.$emit('input', value)
就可以修改传入的值,也可以配合
@Emit
装饰器使用:
@Component({
components: { C },
export default class B extends Vue {
@VModel({ type: String }) name!: string;
@Emit()
input(): string {
return "张三";
5. @Model/@ModelSync
5.1 @Model
从上面的
v-model
双向绑定我们可以得知,
v-model
实际上是绑定了一个Prop value属性,和
$emit
的input属性,如果想将
v-model
所绑定的这两个属性更改一下名字时,我们就可以使用
@Model
装饰器:
import { Vue, Component, Model } from 'vue-property-decorator'
@Component
export default class YourComponent extends Vue {
@Model('change', { type: Boolean }) readonly checked!: boolean
上面的代码等同于:
export default {
model: {
prop: 'checked',
event: 'change',
props: {
checked: {
type: Boolean,
至于model属性,可以参考 官方文档 。
5.2 @ModelSync
该装饰器是结合了
@Model
和
@VModel
两个装饰器,将两个装饰器的功能合二为一。
import { Vue, Component, ModelSync } from 'vue-property-decorator'
@Component
export default class YourComponent extends Vue {
@ModelSync('checked', 'change', { type: Boolean })
readonly checkedValue!: boolean
上面的代码等同于:
export default {
model: {
prop: 'checked',
event: 'change',
props: {
checked: {
type: Boolean,
computed: {
checkedValue: {
get() {
return this.checked
set(value) {
this.$emit('change', value)
6. @Provide/@Inject
其实我之前是不知道有这两个装饰器的,直到我最近写Vue组件中的值传递比较费劲,突然想起Vue中有
Provide/Inject
,所以去翻阅了一下Vue中的几种值传递方式,然后又想到我目前正在写的项目虽然是使用js构建,但是使用了
vue-property-decorator
装饰器库,就去翻阅一下官方文档看是否有这两个装饰器,一翻还真有。
这里说一下
vuex
存在的副作用,以及我为啥想要使用
Provide/Inject
,其实我之前一直是一个
vuex
党,对于一个复杂状态的传递我搞不清楚就统统使用
vuex
,但是有时候
vuex
会将一些简单的逻辑复杂化,因为
vuex
的
state
状态的修改理论上都是需要通过
Mutation
(虽然可以使用对象的方式绕过
Mutation
直接修改
state
的值,就跟
Prop
传递到子组件的对象,子组件也可以修改该对象下的属性一样)。
同时
vuex
还带来一个问题,因为
vuex
中存储的状态是不会因为组件的销毁而自动销毁的,除非刷新浏览器,所以有时候会因为留下了很多状态而引起一些BUG,如果要手动清除这些状态势必又会带来额外的工作量,所以为了解决子孙之间的值传递,我就想到了
Provide/Inject
。
但是使用
Provide/Inject
又会引发另一个问题,就是
Provide传递的值因为可以在任何子组件甚至孙组件中通过Inject来进行调用,所以该值的源头找起来就会比较麻烦,所以一定要做好注释!
不然过段时间再看代码你根本找不到该值从哪个父层级中传递而来。
我曾经接手过一个项目,它是使用
$refs
的方式进行传值,又没有注释,半找半猜终于找到了该值的源头,过程十分艰辛。
6.1 @Provide
该装饰器可以直接声明在类中的变量上,将该属性通过
provide
传递给子组件。
例子:
@Component({
components: { B },
export default class A extends Vue {
@Provide()
message = {
content: "",
相当于:
export default {
components: { B },
data() {
return {
message: { content: "" },
provide() {
return {
message: this.message,
可以看到简化了非常多的代码,非常方便,同时
@Provide()
中还可以跟一个
String
类型的参数,该参数表示了
provide()
所要传递出去的名字,一旦设置在后面的
Inject
中就需要使用设置的别名来调用:
// 父组件
@Component({
components: { B },
export default class A extends Vue {
// Provide a
@Provide("a")
message = {
content: "",
// 子组件
export default {
name: "B",
components: { C },
inject: {
// 这里的值一定要与父组件@Provide()括号中的值对应
message: "a",
如果不设置别名,则会默认将所装饰的变量名当做Provide的名称。
6.2 @Inject
跟上面一样,使用起来非常方便:
@Component
export default class C extends Vue {
@Inject() readonly message!: { content: string };
如果
@Inject()
括号中不带参数,则默认将后面的变量名作为注入名称。
也可以像
@Inject("a")
这样指定对应的名称,同时后面还可以跟一个对象设置其默认值:
@Inject({ from: 'optional', default: 默认值 })
。
7. @ProvideReactive/@InjectReactive
可以看到上面演示
@Provide/@Inject
我是使用了一个
message
对象,然后修改它的
content
属性,这是因为
Provide/Inject
传递的值不是响应式,如果你将
message
对象换成一个字符串,如下所示:
export default class A extends Vue {
@Provide()
message = "";
则当
message
被改变时,子组件中调用该数据的界面显示不会有任何变化,换句话说就是子组件其界面不会刷新,那么这个时候
@ProvideReactive/@InjectReactive
就可以解决这个问题。
// 父组件
export default class A extends Vue {
@ProvideReactive()
message = "";
// 子组件
export default class B extends Vue {
@InjectReactive() message!: string;
你再去页面中尝试,你会发现即使更改了
message
的值,在子组件调用到的界面也会进行刷新。
8. @Watch
这个很简单,就不过多讲解了,一看就会:
// 需要监听的变量名
@Watch("data")
change(): void {
// 所要执行的语句