组件是可复用的 Vue
实例,它们与 new Vue
接收相同的选项,例如 data
、computed
、watch
、methods
以及生命周期钩子等。
比如我们写一个计数器组件。
<template>
<button @click="counter++">点击按钮{{counter}}次button>
template>
<script>
export default {
data() {
return {
counter: 0
}
}
}
script>
在写完一个组件后,需要进行注册才能使用。
在父组件中注册使用:
<template>
<div>
<h1>使用组件h1>
<counter />
div>
template>
<script>
import counter from './counter.vue';
export default {
components: {
counter
}
}
script>
在父组件中引入counter
,之后在componets
进行声明才能使用。
vue2
中是直接使用vue
的原型中component
进行注册.
import Vue from 'vue';
Vue.component('xxx', xxx);
在vue3
中,则是在实例中进行添加
import { createApp } from 'vue'
const app = createApp({})
app.component('xxx', xxx);
全局注册后,就不需要在每个vue
组件声明了,可直接使用。
在前面的一些例子中,你可能已经注意到了data
并不是一个对象,而是一个函数。这是为了防止组件中数据互相影响的问题。
如果data
是一个对象
data: {
counter: 0
}
那么其他组件都引用counter
时,counter
的值变动就会影响到所有的组件。而使用函数,那么在其他引用组件中都是返回一个独立的拷贝对象,不会相互影响。
vue
提供了prop
可以使子组件能够接收父组件传递的数据。
prop
可以在组件上注册的一些自定义数据。当一个值传递给一个prop
的时候,它就变成了那个组件实例的一个值。
在子组件上定义一个prop
为message
,表示子组件可以接收一个message
值。
<template>
<div>
<h1>{{message}}h1>
<button @click="counter++">点击按钮{{counter}}次button>
div>
template>
<script>
export default {
props: {
message: {
type: Number,
default: 'no message'
}
},
data() {
return {
counter: 0
}
}
}
script>
定义好子组件后,在父组件中传值
<template>
<counter :message="title" />
template>
<script>
export default {
data() {
return {
title: '来自父组件传递的消息'
}
}
}
script>
一个组件默认可以拥有任意数量的 prop
,任何值都可以传递给任何 prop
。在上述模板中,你会发现我们能够在组件实例中访问这个值,就像访问 data
中的值一样。
prop
的类型1. 数组
props: ['title', 'content']
使用数组格式的话并不能明确值的类型,不推荐使用
2. 对象
props: {
title: String,
content: String
}
当传递的值类型不符合时会在浏览器中进行提示。
对于父组件而言,当子组件需要传递的prop
太多时,可以使用一个对象进行传递
<template>
<counter v-bind="propObj" />
template>
<script>
export default {
data() {
return {
propObj: {
title: '来自父组件',
content: '内容'
}
}
}
}
script>
prop
类型验证我们可以为组件的 prop
指定验证要求,例如你知道的这些类型。如果有一个需求没有被满足,则 Vue
会在浏览器控制台中警告你。这在开发一个会被别人用到的组件时尤其有帮助。
为了定制 prop
的验证方式,你可以为 props
中的值提供一个带有验证需求的对象,而不是一个字符串数组。
props: {
// 基础类型
prop1: String,
// 多个类型
prop2: [String, Number],
// 必填
prop3: {
type: String,
required: true
},
// 带有默认值
prop4: {
type: Number,
default: 1
},
// 带有默认对象
prop5: {
type: Object,
// 对象或数组默认值必须从一个工厂函数获取
default: function () {
return { message: 'hello' }
}
},
// 自定义验证函数
prop6: {
validator: function (value) {
// 这个值必须匹配下列字符串中的一个
return ['success', 'warning', 'danger'].indexOf(value) !== -1
}
}
}
当 prop
验证失败的时候,(开发环境构建版本的) Vue
将会产生一个控制台的警告。
prop
的type
类型可以是以下几种:
String
Number
Boolean
Array
Object
Date
Function
Symbol
其次,type
还可以是一个自定义的构造函数,并且通过 instanceof
来进行检查确认。
class Animal {
constructor(options) {}
}
props: {
animal: Animal
}
prop
传递的数据是单向的所有的 prop
都使得其父子 prop
之间形成了一个单向下行绑定:父级 prop
的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外变更父级组件的状态,从而导致你的应用的数据流向难以理解。
当需要对prop
传递的值进行更改时,最好在data
中重新定义一个数据并将prop
作为初始值。
props: {
value: Number
},
data() {
return {
counter: this.value
}
}
需要注意的是,如果传递的是数组或者对象,由于父子组件的数据是通过引用传入的,在子组件中改变变更这个对象或数组本身将会影响到父组件的状态。
当父组件传递了一些未在子组件中定义的prop
数据时,在子组件中可以通过this.$attr
访问到那些未声明的属性(除style和class外)
在父组件中传递了多个数据
<template>
<son title="title" content="content" />
template>
在子组件中只定义了一个title
, 可以通过this.$attrs
获取到其他未声明的prop
<template>
<div>
<h1>{{title}}h1>
div>
template>
<script>
export default {
props: {
title: String
},
created() {
console.log(this.$attrs) // { content: "content" }
}
}
script>
同时未声明的prop
也会渲染在子组件的根节点中。
比如上面的例子中content
未声明,子组件的渲染结果为
<div content="content">
<h1>titleh1>
div>
对于一些组件而言,不希望接收未声明的prop
,可以通过设置inheritAttrs: false
。
// son.vue
inheritAttrs: false,
props: {
title: String
},
此时根节点就不会存在未声明的prop
子组件的渲染结果为
<div>
<h1>titleh1>
div>
比如说子组件需要改变prop
,但是自身不能修改这些数据,因此可以通过事件来实现。
<template>
<div>
<h1>{{title}}h1>
<button @click="$emit('changeTitle')">改变titlebutton>
div>
template>
<script>
export default {
props: {
title: String
},
}
script>
使用$emit
可以触发父组件定义在子组件上的方法。
<template>
<son @changeTitle="titleChange" :title="" >
template>
<script>
export default {
data() {
return {
title: '标题'
}
},
titleChange() {
this.title += '1';
}
}
script>
同时$emit
还可以传递参数,$emit('methodName', params)
。
在父组件中接收
titleChange(params) {
console.log(params)
}
和 HTML
元素一样,我们经常需要向一个组件传递内容。
<son>
<h2>插槽的使用h2>
son>
在子组件中使用
来安排插槽的位置
<template>
<div>
<h1>{{title}}h1>
<slot>slot>
div>
template>
当组件渲染时,
将会被替换为
。插槽的使用
父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。
虽然插槽时用来替换父组件中的元素,但是插槽中的只能访问父组件中定义的数据,不能访问子组件的。
在父组件中没有定义value
<son>
<h2>插槽的使用 {{value}}h2>
son>
但是在子组件中定义了value
// son.vue
data() {
return {
value: 1
}
}
这时访问value
会报错。
你也可以理解成插槽默认值
<template>
<div>
<h1>{{title}}h1>
<slot>dafault textslot>
div>
template>
当父组件没有传入任何插槽内容时,默认会渲染出一个文本节点(内容为slot
标签内的文本)
渲染结果
<div>
<h1>titleh1>
dafault text
div>
有时候在组件中可能传入多个插槽,为了将插槽放入正确的位置,我们需要给插槽命名。
比如我们有一个layout
组件
<div class="container">
<header>
header>
<main>
main>
<footer>
footer>
div>
如果没有给slot
命名,直接插入3个slot
<div class="container">
<header>
<slot>slot>
header>
<main>
<slot>slot>
main>
<footer>
<slot>slot>
footer>
div>
在父组件中传值
<layout>
<div>headerdiv>
<div>contentdiv>
<div>footerdiv>
layout>
那么layout
中的所有的内容都会被当成一个名为dafault
的slot
,最终的渲染结果为
<div class="container">
<header>
<div>headerdiv>
<div>contentdiv>
<div>footerdiv>
header>
<main>
<div>headerdiv>
<div>contentdiv>
<div>footerdiv>
main>
<footer>
<div>headerdiv>
<div>contentdiv>
<div>footerdiv>
footer>
div>
因此我们可以给每个slot
添加一个name
属性用来保证slot
被插入到正确的位置。
<div class="container">
<header>
<slot name="header">slot>
header>
<main>
<slot>slot>
main>
<footer>
<slot name="footer">slot>
footer>
div>
在父组件中给标签添加slot
属性,值与子组件中的name
值相等。
<layout>
<div slot="header">headerdiv>
<div>contentdiv>
<div slot="footer">footerdiv>
layout>
通过具名即使在父组件中我们把插槽的顺序打乱,最终的渲染结果也没啥问题。未传slot
值默认为dafault
。
<layout>
<div>contentdiv>
<div slot="footer">footerdiv>
<div slot="header">headerdiv>
layout>
但是由于vue2.6.0
引入v-slot
指令,能够提供更好的支持 slot
和 slot-scope
的 API
替代方案。而且vue3
已经废弃使用slot
来给插槽命名,因此不在推荐使用slot
的方式,而是改用v-slot
指令。
在向具名插槽提供内容的时候,我们可以在一个元素上使用 v-slot
指令,并以 v-slot
的参数的形式提供其名称。
<layout>
<template v-slot:header>
<div>headerdiv>
template>
<div>contentdiv>
<template v-slot:footer>
<div>footerdiv>
template>
layout>
任何没有被包裹在带有 v-slot
的元素中的内容都会被视为默认插槽的内容。
注意 v-slot
只能添加在 上(除以下例子外),这点与
slot
不同
独占默认插槽的缩写语法
当被提供的内容只有默认插槽时,组件的标签才可以被当作插槽的模板来使用。
<current-user v-slot:default="slotProps">
{{ slotProps.user.firstName }}
current-user>
在前面的例子我们提到插槽不能访问到子组件中的data
。通过在子组件中绑定数据,我们也能解决这个问题。
子组件中使用v-bind
指令给slot
绑定了一个user
数据
<span>
<slot v-bind:user="user">
{{ user.lastName }}
slot>
span>
在父组件中我们使用v-slot="slotProps"
接收子组件传递过来的数据。
<current-user>
<template v-slot:default="slotProps">
{{ slotProps.user.firstName }}
template>
current-user>
作用域插槽的内部工作原理是将你的插槽内容包裹在一个拥有单个参数的函数里。
function (slotProps) {
// 插槽内容
}
这意味着 v-slot
的值实际上可以是任何能够作为函数定义中的参数的 js
表达式,因此我们可以使用ES6
的解构来更好的获取数据.
<current-user v-slot="{ user }">
{{ user.firstName }}
current-user>
跟其他指令一样,v-slot
也能接收到动态的插槽名。
<base-layout>
<template v-slot:[dynamicSlotName]>
...
template>
base-layout>
跟 v-on
和 v-bind
一样,v-slot
也有缩写: #
<layout>
<template #header>
<div>headerdiv>
template>
<div>contentdiv>
<template #footer>
<div>footerdiv>
template>
layout>
使用
标签,并使用is
属性来切换不同的组件
<component v-bind:is="currentTabComponent">component>
<script>
import Com1 from 'xxx';
export default {
data() {
return {
currentTabComponent: Com1
}
}
}
script>
在大型应用中,我们可能需要将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载一个模块。为了简化,Vue
允许你以一个工厂函数的方式定义你的组件,这个工厂函数会异步解析你的组件定义。Vue
只有在这个组件需要被渲染的时候才会触发该工厂函数,且会把结果缓存起来供未来重渲染。
Vue.component('async-example', function (resolve, reject) {
setTimeout(function () {
// 向 `resolve` 回调传递组件定义
resolve({
template: '异步组件'
})
}, 1000)
})
在注册的时候也需要动态导入
Vue.component(
'async-webpack-example',
// 这个动态导入会返回一个 `Promise` 对象。
() => import('./my-async-component')
)
当使用局部注册的时候,你也可以直接提供一个返回 Promise
的函数:
new Vue({
// ...
components: {
'my-component': () => import('./my-async-component')
}
})