4.3 组件体系
前面介绍了 Vue.js 的基本语法和组件的基础用法。组件的两大特性为独立性和可复用性。在一个项目中,往往由多个组件互相嵌套组成复杂的组件树,这就需要组件之间有数据传递和事件响应。
下面从组件复用的角度,结合组件原理深入地介绍组件。
4.3.1 data 与 props
组件的状态在 data 中定义,表示组件的内部状态。当一个组件需要复用时,必然会有状态从外部传入,这类状态被称为 props。
组件允许接收哪些 props,需要在组件内部定义。props 需要使用 props 属性来定义,属性值为允许接收的 props 字段名:
// Demo.vue
export default {
props: ["foot"],
created() {
console.log(this.foot);
},
};
上面代码中,我们定义了一个名为 foot 的 props。props 与 data 一样也通过 this 访问。
定义之后,在使用组件时,props 就可以像普通属性一样定义在组件上,如下:
<Demo foot="name"></Demo>
有时候我们需要限制 props 必须是指定类型,不可以随意传递。这个时候可以使用第二种 props 定义方式:对象定义。上面组件代码修改如下:
export default {
props: {
foot: {
type: String,
},
},
};
使用对象定义 foot 的类型,使用组件时就能规范 props 值的传递。对象定义不光可以定义类型,还可以定义更多的验证规则,如下:
export default {
props: {
foot: {
type: String,
required: true,
default: "",
validator(value) {
return ["val1", "val2"].includes(value);
},
},
},
};
如上代码中,props 定义对象的各个属性含义如下:
type
:数据类型,值为 JavaScript 原生构造函数,如 String,Boolean。required
:是否必传。default
:默认值。validator
:验证函数,自动义验证规则。
props 的多种定义方式,有的简单有的复杂。不过为了组件的健壮性,推荐使用更加严格的验证。
4.3.2 自定义事件
Vue 的自定义事件由子组件触发,父组件监听,从而实现向上的事件响应。
在子组件中,通过 this.$emit() 方法触发一个自定义事件;
// Child.vue
<button @click="$emit('updateMsg')">click me</button>
接着在父组件中,通过 v-on 指令来监听这个事件:
// Parent.vue
<div>
<Child @update-msg="updateMsg"></Child>
</div>
export default {
methods: {
updateMsg() {
console.log('自定义事件触发')
}
}
}
子组件触发自定义事件需要指定一个事件名,父组件监听这个事件名。与 props 一样,事件名也支持自动格式转换。上面代码中子组件触发 updateMsg 事件,父组件可以用 update-msg 来监听。
自定义事件还支持传递参数。子组件将参数传递给父组件,父组件可以在事件触发函数中收到:
// Child.vue
<button @click="$emit('updateMsg', 'hello')">click me</button>
// Parent.vue
<Child @update-msg="updateMsg"></Child>
methods: {
updateMsg(msg) {
console.log(msg)
}
}
使用自定义事件修改 props 也很简单,我们写一个完整案例。子组件代码如下:
<template>
<div class="child-component">
<h2>{{ userName }}</h2>
<button @click="changeName">修改</button>
</div>
</template>
<script>
export default {
props: {
userName: {
type: String,
required: true,
},
},
methods: {
changeName() {
this.$emit("updateName", "王小五");
},
},
};
</script>
子组件接收 userName 属性,并触发 updateName 事件,父组件代码如下:
<template>
<div class="parent-component">
<Child :user-name="userName" @update-name="changeName" />
</div>
</template>
<script>
import Child from "./child.vue";
export default {
data() {
return {
userName: "张小四",
};
},
component: {
Child,
},
methods: {
changeName(name) {
this.userName = name;
},
},
};
</script>
当点击子组件按钮时,父组件的函数被触发,并修改父组件的状态,此时子组件的状态也会自动变化。
4.3.3 生命周期
Vue 中的每个组件从创建到销毁都有一个完整的过程,这个过程被称为生命周期。
组件的生命周期意义非凡。Vue 提供了许多生命周期函数,允许我们在组件的不同的阶段做不同的事情。比如,在组件创建后发起 API 请求就是一个常见的需求。
组件的全部完整的生命周期如图 5-1-4:
上图中的声明周期较多,实际场景下我们常用的声明周期只有 4 个,列举如下:
- created():组件实例创建后触发,此时 DOM 未挂载。
- mounted():DOM 挂载后触发,组件彻底初始化完成。
- updated():组件更新后出发,可获得更新后的状态和 DOM。
- unmounted():组件卸载之前触发。
created() 是组件实例创建之后,最早触发的声明周期,因此在这里发起网络请求非常合适。
mounted() 在组件被挂载并生成 DOM 之后触发,如果要操作 DOM,在这个声明周期中最合适。切记不可在 created() 中操作 DOM,因此此时 DOM 还不存在。
组件中的状态更新会导致组件更新,组件更新后会触发 updated()。但是这个声明周期不常用,监听状态变化我们一般用监听器来实现更精准。
unmounted() 在组件卸载前触发,这个声明周期很有用。比如要清理定时器,关闭某种连接,重置全局状态等,都可以在这个声明周期内操作。
其他的生命周期钩子可能在特殊时候会用到,但这 4 个已经满足绝大部分要求了。
4.3.4 插槽动态渲染
组件可以通过 props 接收到任意类型的 JavaScript 数据,依据 props 可以动态渲染模版。
对于一些定制程度高、灵活性强的组件,我们更希望直接接收一个模版,这样比接收数据再渲染模版更加灵活。Vue 提供了一个“插槽”功能用于接收模版,插槽用 <slot></slot>
来表示。
假设现在有一个卡片组件 Card.vue,组件定义卡片的通用外部样式,卡片内可以填充任意内容,此类场景就非常适合用插槽。
// Card.vue
<div class="card-box">
<h2>卡片</h2>
<div>
<slot></slot>
</div>
</div>
使用卡片组件时,用卡片组件包裹任意内容,这些内容会替换组件的插槽部分,最终被卡片组件渲染。举例如下:
// 使用组件
<Card>卡片内容</Card>
// 渲染后的DOM
<div class="card-box">
<h2>卡片</h2>
<div>卡片内容</div>
</div>
// 使用组件
<Card>
<div>卡片模版</div>
</Card>
// 渲染后的DOM
<div class="card-box">
<h2>卡片</h2>
<div>
<div>卡片模版</div>
</div>
</div>
插槽内容可以是文本、元素、组件,非常灵活,这些都由父组件提供。
对于一些复杂组件,可能不止一处需要插入模版。还是以上面的卡片组件为例,假设卡片头部需要自定义,卡片内容也需要自定义,这个时候就需要两处插槽。那么传入组件的模版应该替换哪个插槽呢?
为了在一个组件内区分多个插槽,Vue 提供了具名插槽。具体做法是:为 <slot>
元素添加一个 name 属性,这样就能区分了。
// Card.vue
<div class="card-box">
<div class="header">
<slot name="header"></slot>
</div>
<div class="content">
<slot name="content"></slot>
</div>
</div>
在使用 Card 组件时,组件可包裹 <template>
元素并配合 v-slot 指令与组件内的具名插槽匹配,实现如下:
<Card>
<template v-slot:header>
<h2>卡片标题</h2>
</template>
<template v-slot:content>
<div>卡片内容</div>
</template>
</Card>
指令 v-slot 可以用 # 符号简写,如下:
<template #header>
<h2>卡片标题</h2>
</template>
组件内也可以将默认插槽和具名插槽结合使用,如果只有一处插槽,请使用默认插槽。
4.3.5 异步组件
在大型项目中,组件庞大会导致页面加载缓慢,这是一次性加载所有组件带来的性能问题。我们希望组件可以按需加载,进入页面时只加载需要的组件,这样会大大提升性能。
Vue 中按需加载的组件被称为异步组件。Vue 提供了 defineAsyncComponent() 方法来实现此功能:
import { defineAsyncComponent } from "vue";
const AsyncComp = defineAsyncComponent(() => {
import("./TestComponent.vue");
});
如上,异步加载组件时还需要 import() 方法配合。加载之后,这个组件可以像普通组件一样使用:
<div>
<AsyncComp></AsyncComp>
</div>
在单文件组件中,也可以用 defineAsyncComponent() 方法来异步加载一个组件:
export default {
components: {
AsyncComp = defineAsyncComponent(() =>
import('./TestComponent.vue')
)
}
}
4.3.6 实现 v-model
前面介绍过,v-model 是一个语法糖,内部封装了值绑定和事件触发。很多时候自定义组件也需要双向绑定功能,该如何实现呢?
以 input 为例:绑定值的字段是 value,事件是 input,那么在组件内的定义方式如下:
// input
export default {
name: "input",
model: {
prop: "value", // 对应 props value
event: "input",
},
props: {
value: String,
},
};
代码可见,在组件中使用 model 属性来指定 v-model 指令关联的状态和事件。
假设现在有组件 CusModal.vue,我们定义如下:
// CusModal.vue
export default {
name: "CusModal",
model: {
prop: "visible", // 对应 props value
event: "toggle",
},
props: {
visible: Boolean,
},
};
现在这个组件绑定的 props 和事件分别是 visible 和 toggle。使用这个组件时,以下两种方式是一样的:
data() {
return { value }
}
// 方式一
<CusModal :visible="value" @toggle="v=> value=v"></CusModal>
// 方式二
<CusModal v-model="value"></CusModal>