组件注册
全局注册
通过app.component注册,所有子组件都可以使用。
import MyComponent from './App.vue'
app.component('MyComponent', MyComponent)
缺点:
- 全局注册但并没有被使用的组件tree-shaking时无法移除,增加打包体积;
- 大型项目中使项目的依赖关系变得不那么明确;
局部注册
通过components选项注册,当前文件可用,依赖关系明确。
<script>
import ComponentA from './ComponentA.vue'
export default {
components: {
ComponentA
}
}
</script>
<template>
<ComponentA />
</template>
Props
Props声明
一个组件需要显式声明它所接受的 props,这样 Vue 才能知道外部传入的哪些是 props,哪些是透传 attribute。
父组件:
<script>
import ComponentA from './ComponentA.vue'
export default {
components: {
ComponentA
}
}
</script>
<template>
<ComponentA title='hello vue3' class='demo' />
</template>
子组件:
<script>
export default {
props: {
title: String,
},
}
</script>
<template>
<div class='child-wrapper'>{{title}}</div>
</template>
以上代码中,title是prop, class是透传attribute, 子组件模板最终就是这样:
<template>
<div class='child-wrapper demo'>{{title}}</div>
</template>
静态和动态Prop
使用v-bind绑定的属性动态prop,其他的是静态prop。
<ComponentA title='hello vue3' />
<ComponentA :title='doc.title' />
多个动态Prop
将一个对象的所有属性作为prop传给子组件,使用不带参数的v-bind
export default {
data() {
return {
post: {
id: 1,
title: 'My Journey with Vue'
}
}
}
}
<template>
<BlogPost v-bind="post" />
/*
<BlogPost :id="post.id" :title="post.title" />
*/
</template>
Prop校验
每个prop可以设置type、default、validator、required约束。对于数组、对象类型的prop,默认值是返回数组或对象的函数;对于函数类型的prop, 默认值是普通函数。
<script>
export default {
props: {
// 基础类型检查
//(给出 `null` 和 `undefined` 值则会跳过任何类型检查)
propA: Number,
// 多种可能的类型
propB: [String, Number],
// 必传,且为 String 类型
propC: {
type: String,
required: true
},
// Number 类型的默认值
propD: {
type: Number,
default: 100
},
// 对象类型的默认值
propE: {
type: Object,
// 对象或者数组应当用工厂函数返回。
// 工厂函数会收到组件所接收的原始 props
// 作为参数
default(rawProps) {
return { message: 'hello' }
}
},
// 自定义类型校验函数
propF: {
validator(value) {
// The value must match one of these strings
return ['success', 'warning', 'danger'].includes(value)
}
},
// 函数类型的默认值
propG: {
type: Function,
// 不像对象或数组的默认,这不是一个
// 工厂函数。这会是一个用来作为默认值的函数
default() {
return 'Default function'
}
}
}
}
</script>
<template>
<div>111</div>
</template>
组件事件
触发与监听事件
子组件通过$emit
方法触发自定义事件,父组件通过v-on
监听自定义事件。
子组件:
<button @click="$emit('increaseBy', 1)"> Increase by 1 </button>
父组件:
<template>
<div>count: {{count}}</div>
<MyButton @increase-by="increaseCount" />
</template>
<script>
import MyButton from './MyButton.vue'
export default {
components: { MyButton },
data() {
return {
count: 0,
}
},
methods: {
increaseCount(n) {
this.count += n
}
},
}
</script>
事件声明和检验
组件可以显式地通过 emits
选项来声明它要触发的事件;和对 props 添加类型校验的方式类似,所有触发的事件也可以使用对象形式来描述。
要为事件添加校验,那么事件可以被赋值为一个函数,接受的参数就是抛出事件时传入 this.$emit
的内容,返回一个布尔值来表明事件是否合法。
export default {
emits: {
// 没有校验
click: null,
// 校验 submit 事件
submit: ({ email, password }) => {
if (email && password) {
return true
} else {
console.warn('Invalid submit event payload!')
return false
}
}
},
methods: {
submitForm(email, password) {
this.$emit('submit', { email, password })
}
}
}
组件v-model
回顾v-model
v-model是v-bind和v-on的语法糖。首先让我们回忆一下 v-model
在原生元素上的用法:
<input v-model="searchText" />
在代码背后,模板编译器会对 v-model
进行更冗长的等价展开。因此上面的代码其实等价于下面这段:
<input
:value="searchText"
@input="searchText = $event.target.value"
/>
而当使用在一个组件上时<CustomInput v-model="searchText" />
,v-model
会被展开为如下的形式:
<CustomInput
:modelValue="searchText"
@update:modelValue="newValue => searchText = newValue"
/>
<!-- CustomInput.vue -->
<script>
export default {
props: ['modelValue'],
emits: ['update:modelValue']
}
</script>
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</template>
另一种在组件内实现 v-model
的方式是使用一个可写的,同时具有 getter 和 setter 的 computed
属性。get
方法需返回 modelValue
prop,而 set
方法需触发相应的事件:
<!-- CustomInput.vue -->
<script>
export default {
props: ['modelValue'],
emits: ['update:modelValue'],
computed: {
value: {
get() {
return this.modelValue
},
set(value) {
this.$emit('update:modelValue', value)
}
}
}
}
</script>
<template>
<input v-model="value" />
</template>
v-model
的参数
默认情况下,v-model
在组件上都是使用 modelValue
作为 prop,并以 update:modelValue
作为对应的事件。可以指定一个参数来更改这些名字。
<MyComponent v-model:title="bookTitle" />
子组件应声明一个 title
prop,并通过触发 update:title
事件更新父组件值:
<!-- MyComponent.vue -->
<script>
export default {
props: ['title'],
emits: ['update:title']
}
</script>
<template>
<input
type="text"
:value="title"
@input="$emit('update:title', $event.target.value)"
/>
</template>
v-model
修饰符
我们来创建一个自定义的修饰符 capitalize
,它会自动将 v-model
绑定输入的字符串值第一个字母转为大写:
<MyComponent v-model.capitalize="myText" />
组件的 v-model
上所添加的修饰符,可以通过 modelModifiers
prop 在组件内访问到。在下面的组件中,我们声明了 modelModifiers
这个 prop,它的默认值是一个空对象:
<script>
export default {
props: {
modelValue: String,
modelModifiers: {
default: () => ({})
}
},
emits: ['update:modelValue'],
methods: {
emitValue(e) {
let value = e.target.value
if (this.modelModifiers.capitalize) {
value = value.charAt(0).toUpperCase() + value.slice(1)
}
this.$emit('update:modelValue', value)
}
}
}
</script>
<template>
<input type="text" :value="modelValue" @input="emitValue" />
</template>
对于又有参数又有修饰符的 v-model
绑定,生成的 prop 名将是 arg + "Modifiers"
。举例来说:
<MyComponent v-model:title.capitalize="myText">
相应的声明应该是:
export default {
props: {
title: String,
titleModifiers: {
default: () => ({})
}
},
emits: ['update:title'],
}
透传 Attributes
Attributes 继承
“透传 attribute”指的是传递给一个组件,却没有被该组件声明为 props 或 emits 的 attribute 或者 v-on
事件监听器。最常见的例子就是 class
、style
和 id
。
当一个组件以单个元素为根作渲染时,透传的 attribute 会自动被添加到根元素上。
<!-- <MyButton> 的模板 -->
<button>click me</button>
一个父组件使用了这个组件,并且传入了 class
:
<MyButton class="large" />
最后渲染出的 DOM 结果是:
<button class="large">click me</button>
这里,<MyButton>
并没有将 class
声明为一个它所接受的 prop,所以 class
被视作透传 attribute,自动透传到了 <MyButton>
的根元素上。
对class、style、v-on
的继承和合并
<!-- <MyButton> 的模板 -->
<button class="btn" @click='selfClickHandler'>click me</button>
父组件使用了该组件:
<MyButton class="large" @click="onClick" />
则最后渲染出的 DOM 结果会变成:
<button class="btn large" @click="selfClickHandler(); onClick()">click me</button>
深层组件继承
有些情况下一个组件会在根节点上渲染另一个组件。例如,我们重构一下 <MyButton>
,让它在根节点上渲染 <BaseButton>
:
<!-- <MyButton/> 的模板,只是渲染另一个组件 -->
<BaseButton />
此时 <MyButton>
接收的透传 attribute 会直接继续传给 <BaseButton>
。
请注意:
- 透传的 attribute 不会包含
<MyButton>
上声明过的 props 或是针对emits
声明事件的v-on
侦听函数,换句话说,声明过的 props 和侦听函数被<MyButton>
“消费”了。 - 透传的 attribute 若符合声明,也可以作为 props 传入
<BaseButton>
。
禁用 Attributes 继承
如果你不想要一个组件自动地继承 attribute,你可以在组件选项中设置 inheritAttrs: false
。
这些透传进来的 attribute 可以用 $attrs
访问到。这个 $attrs
对象包含了除组件所声明的 props
和 emits
之外的所有其他 attribute,例如 class
,style
,v-on
监听器等等。
有几点需要注意:
- 和 props 有所不同,透传 attributes 在 JavaScript 中保留了它们原始的大小写,所以像
foo-bar
这样的一个 attribute 需要通过$attrs['foo-bar']
来访问。 - 像
@click
这样的一个v-on
事件监听器将在此对象下被暴露为一个函数$attrs.onClick
。
现在我们要再次使用一下 <MyButton>
组件例子。有时候我们可能为了样式,需要在 <button>
元素外包装一层 <div>
:
<div class="btn-wrapper">
<button class="btn">click me</button>
</div>
我们想要所有像 class
和 v-on
监听器这样的透传 attribute 都应用在内部的 <button>
上而不是外层的 <div>
上。我们可以通过设定 inheritAttrs: false
和使用 v-bind="$attrs"
来实现:
<div class="btn-wrapper">
<button class="btn" v-bind="$attrs">click me</button>
</div>
多根节点的 Attributes 继承
有着多个根节点的组件没有自动 attribute 透传行为,如果 $attrs
没有被显式绑定,将会抛出一个运行时警告,因为Vue 不知道要将 attribute 透传到哪里。
<CustomLayout id="custom-layout" @click="changeValue" />
如果 <CustomLayout>
有下面这样的多根节点模板,会抛出一个警告。
<header>...</header>
<main>...</main>
<footer>...</footer>
如果 $attrs
被显式绑定,则不会有警告:
<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>