一、组件注册
方式:全局注册和局部注册。
1、全局注册
app.component()方法可以被链式调用:
app
.component('ComponentA', ComponentA)
.component('ComponentB', ComponentB)
.component('ComponentC', ComponentC)
存在几点问题:
全局注册,但并没有被使用的组件无法在生产打包时被自动移除 (也叫“tree-shaking”)。
全局注册在大型项目中使项目的依赖关系变得不那么明确。
2、局部注册
优点是使组件之间的依赖关系更加明确,并且对 tree-shaking 更加友好
在使用 <script setup> 的单文件组件中,导入的组件可以直接在模板中使用,无需注册
vue
<script setup>
import ComponentA from './ComponentA.vue'
</script>
<template>
<ComponentA />
</template>
如果没有使用 <script setup>,则需要使用 components 选项来显式注册:
3、组件名格式
建议使用 <PascalCase />作为组件名的注册格式
二、Props
1、Props 声明
// <script setup> 使用 defineProps() 宏来声明:
vue
<script setup>
const props = defineProps(['foo'])
console.log(props.foo)
</script>
// 非 <script setup> 使用 props 选项来声明:
js
export default {
props: ['foo'],
setup(props) {
// setup() 接收 props 作为第一个参数
console.log(props.foo)
}
}
// 使用对象的形式:
js
// 使用 <script setup>
defineProps({
title: String,
likes: Number
})
js
// 非 <script setup>
export default {
props: {
title: String,
likes: Number
}
}
2、Prop 名字格式
使用 camelCase 形式
3、使用一个对象绑定多个 prop
使用没有参数的 v-bind将一个对象的所有属性都当作 props 传入
js
const post = {
id: 1,
title: 'My Journey with Vue'
}
以及下面的模板:
template
<BlogPost v-bind="post" />
// 而这实际上等价于:
template
<BlogPost :id="post.id" :title="post.title" />
js
defineProps({
// 基础类型检查
// (给出 `null` 和 `undefined` 值则会跳过任何类型检查)
propA: Number,
// 多种可能的类型
propB: [String, Number],
// 必传,且为 String 类型
propC: {
type: String,
required: true
},
// Number 类型的默认值
propD: {
type: Number,
default: 100
},
// 对象类型的默认值
propE: {
type: Object,
// 对象或数组的默认值
// 必须从一个工厂函数返回。
// 该函数接收组件所接收到的原始 prop 作为参数。
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'
}
}
})
TIP
1、defineProps() 宏中的参数不可以访问 <script setup> 中定义的其他变量,因为在编译时整个表达式都会被移到外部的函数中。
另外,type 也可以是自定义的类或构造函数,Vue 将会通过 instanceof 来检查类型是否匹配。例如:
js
class Person {
(firstName, lastName) {
this.firstName = firstName
this.lastName = lastName
}
}
你可以将其作为一个 prop 的类型:
js
defineProps({
author: Person
})
2、Boolean 类型转换
为了更贴近原生 boolean attributes 的行为,声明为 Boolean 类型的 props 有特别的类型转换规则。以带有如下声明的 组件为例:
js
defineProps({
disabled: Boolean
})
该组件可以被这样使用:
template
<!-- 等同于传入 :disabled="true" -->
<MyComponent disabled />
<!-- 等同于传入 :disabled="false" -->
<MyComponent />
当一个 prop 被声明为允许多种类型时,例如:
js
defineProps({
disabled: [Boolean, Number]
})
无论声明类型的顺序如何,Boolean 类型的特殊转换规则都会被应用。
三、组件事件
1、触发与监听事件
和原生 DOM 事件不一样,组件触发的事件没有冒泡机制,只能监听直接子组件触发的事件。
平级组件或是跨越多层嵌套的组件间通信,应使用一个外部的事件总线,或是使用一个全局状态管理方案。
2、声明触发的事件
组件可以显式地通过 defineEmits() 宏来声明它要触发的事件,必须直接放置在 <script setup> 的顶级作用域下。
我们在 <template> 中使用的 $emit 方法不能在组件的 <script setup> 部分中使用,但 defineEmits() 会返回一个相同作用的函数供我们使用:
vue
<script setup>
const emit = defineEmits(['inFocus', 'submit'])
function buttonClick() {
emit('submit')
}
</script>
使用setup 函数,事件需要通过 emits 选项来定义,emit 函数也被暴露在 setup() 的上下文对象上 与 setup() 上下文对象中的其他属性一样,emit 可以安全地被解构
js
export default {
emits: ['inFocus', 'submit'],
setup(props, ctx) {
ctx.emit('submit')
}
}
emits 选项还支持对象语法,它允许我们对触发事件的参数进行验证:
vue
<script setup>
const emit = defineEmits({
submit(payload) {
// 通过返回值为 `true` 还是为 `false` 来判断
// 验证是否通过
}
})
</script>
TIP
如果一个原生事件的名字 (例如 click) 被定义在 emits 选项中,则监听器只会监听组件触发的 click 事件而不会再响应原生的 click 事件。
3、事件校验
和对 props 添加类型校验的方式类似,所有触发的事件也可以使用对象形式来描述。
要为事件添加校验,那么事件可以被赋值为一个函数,接受的参数就是抛出事件时传入 emit 的内容,返回一个布尔值来表明事件是否合法。
vue
<script setup>
const emit = defineEmits({
// 没有校验
click: null,
// 校验 submit 事件
submit: ({ email, password }) => {
if (email && password) {
return true
} else {
console.warn('Invalid submit event payload!')
return false
}
}
})
function submitForm(email, password) {
emit('submit', { email, password })
}
</script>
四、组件 v-model
1、v-model的实现
当使用在一个组件上时,v-model 会被展开为如下的形式:
template
<CustomInput v-model="searchText" />
template
<CustomInput
:modelValue="searchText"
@update:modelValue="newValue => searchText = newValue"
/>
vue
<!-- CustomInput.vue -->
<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</template>
另一种在组件内实现 v-model 的方式是使用一个可写的,同时具有 getter 和 setter 的 computed 属性。get 方法需返回 modelValue prop,而 set 方法需触发相应的事件:
vue
<!-- CustomInput.vue -->
<script setup>
import { computed } from 'vue'
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
const value = computed({
get() {
return props.modelValue
},
set(value) {
emit('update:modelValue', value)
}
})
</script>
<template>
<input v-model="value" />
</template>
2、v-model 的参数
默认情况下,v-model 在组件上都是使用 modelValue 作为 prop,并以 update:modelValue 作为对应的事件。
通过给 v-model 指定一个参数来更改这些名字:
v-model:title="bookTitle"
子组件应声明一个 title prop,并通过触发 update:title 事件更新父组件值:
3、多个 v-model 绑定
组件上的每一个 v-model 都会同步不同的 prop,而无需额外的选项:
template
<UserName
v-model:first-name="first"
v-model:last-name="last"
/>
vue
<script setup>
defineProps({
firstName: String,
lastName: String
})
defineEmits(['update:firstName', 'update:lastName'])
</script>
4、 处理 v-model 修饰符
组件的 v-model 上所添加的修饰符,可以通过 modelModifiers prop 在组件内访问到
template
<MyComponent v-model.capitalize="myText" />
vue
<script setup>
const props = defineProps({
modelValue: String,
modelModifiers: { default: () => ({}) }
})
const emit = defineEmits(['update:modelValue'])
function emitValue(e) {
let value = e.target.value
if (props.modelModifiers.capitalize) {
value = value.charAt(0).toUpperCase() + value.slice(1)
}
emit('update:modelValue', value)
}
</script>
<template>
<input type="text" :value="modelValue" @input="emitValue" />
</template>
对于又有参数又有修饰符的 v-model 绑定,生成的 prop 名将是 arg + "Modifiers"。举例来说:
template
<MyComponent v-model:title.capitalize="myText">
相应的声明应该是:
js
const props = defineProps(['title', 'titleModifiers'])
defineEmits(['update:title'])
console.log(props.titleModifiers) // { capitalize: true }
五、透传 Attributes
1、Attributes 继承
透传 attribute”指的是传递给一个组件,却没有被该组件声明为 props 或 emits 的 attribute 或者 v-on 事件监听器。最常见的例子就是 class、style 和 id。
当一个组件以单个元素为根作渲染时,透传的 attribute 会自动被添加到根元素上。
2、对 class 和 style 的合并
如果一个子组件的根元素已经有了 class 或 style attribute,它会和从父组件上继承的值合并。
template
<MyButton @click="onClick" />
click 监听器会被添加到 <MyButton> 的根元素,即那个原生的 <button> 元素之上。
当原生的 <button> 被点击,会触发父组件的 onClick 方法
如果原生 button 元素自身也通过 v-on 绑定了一个事件监听器,则这个监听器和从父组件继承的监听器都会被触发。
3、禁用 Attributes 继承
在组件选项中设置 inheritAttrs: false。
如果使用了
vue
<script>
// 使用普通的 <script> 来声明选项
export default {
inheritAttrs: false
}
</script>
<script setup>
// ...setup 部分逻辑
</script>
透传进来的 attribute 可以在模板的表达式中直接用 $attrs 访问到。
$attrs 对象包含了除组件所声明的 props 和 emits 之外的所有其他 attribute,例如 class,style,v-on 监听器等等。
template
<span>Fallthrough attribute: {{ $attrs }}</span>
注意:
透传 attributes 在 JavaScript 中保留了它们原始的大小写,所以像 foo-bar 这样的一个 attribute 需要通过 $attrs['foo-bar'] 来访问。
像 @click 这样的一个 v-on 事件监听器将在此对象下被暴露为一个函数 $attrs.onClick。
现在我们要再次使用一下之前小节中的 <MyButton> 组件例子。有时候我们可能为了样式,需要在 <button> 元素外包装一层 <div>:
template
<div class="btn-wrapper">
<button class="btn" v-bind="$attrs">click me</button>
</div>
想要所有像 class 和 v-on 监听器这样的透传 attribute 都应用在内部的 <button> 上而不是外层的 <div> 上,可以通过设定 inheritAttrs: false 和使用 v-bind="$attrs" 来实现:
4、多根节点的 Attributes 继承
和单根节点组件有所不同,有着多个根节点的组件没有自动 attribute 透传行为。
如果 $attrs 没有被显式绑定,将会抛出一个运行时警告。
template
<header>...</header>
<main>...</main>
<footer>...</footer>
// 如果 $attrs 被显式绑定,则不会有警告:
template
<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>
在 JavaScript 中访问透传 Attributes
如果需要,你可以在
vue
<script setup>
import { useAttrs } from 'vue'
const attrs = useAttrs()
</script>
如果没有使用 <script setup>,attrs 会作为 setup() 上下文对象的一个属性暴露:
js
export default {
setup(props, ctx) {
// 透传 attribute 被暴露为 ctx.attrs
console.log(ctx.attrs)
}
}
注意: 虽然这里的 attrs 对象总是反映为最新的透传 attribute,但它并不是响应式的 (考虑到性能因素)。不能通过侦听器去监听它的变化。
如果需要响应性:
1、可以使用 prop。
2、使用 onUpdated() 使得在每次更新时结合最新的 attrs 执行副作用。
六、插槽 Slots
1、渲染作用域
插槽内容可以访问到父组件的数据作用域,无法访问子组件的数据,因为插槽内容本身是在父组件模板中定义的。
举例来说:
template
<span>{{ message }}</span>
<FancyButton>{{ message }}</FancyButton>
这里的两个 {{ message }} 插值表达式渲染的内容都是一样的。
2、动态插槽名
动态指令参数在 v-slot 上也是有效的,即可以定义下面这样的动态插槽名:
template
<base-layout>
<template v-slot:[dynamicSlotName]>
...
</template>
<!-- 缩写为 -->
<template #[dynamicSlotName]>
...
</template>
</base-layout>
注意这里的表达式和动态指令参数受相同的语法限制。
3、作用域插槽
当需要接收插槽 props 时,默认插槽和具名插槽的使用方式有一些小区别。
默认插槽,通过子组件标签上的 v-slot 指令,直接接收到了一个插槽 props 对象:
template
<!-- <MyComponent> 的模板 -->
<div>
<slot :text="greetingMessage" :count="1"></slot>
</div>
template
<MyComponent v-slot="slotProps">
{{ slotProps.text }} {{ slotProps.count }}
</MyComponent>
template
<MyComponent v-slot="{ text, count }">
{{ text }} {{ count }}
</MyComponent>
4、具名作用域插槽
插槽 props 可以作为 v-slot 指令的值被访问到:v-slot:name="slotProps"
template
<MyComponent>
<template #header="headerProps">
{{ headerProps }}
</template>
<template #default="defaultProps">
{{ defaultProps }}
</template>
</MyComponent>
向具名插槽中传入 props:
template
<slot name="header" message="hello"></slot>
注意插槽上的 name 是一个 Vue 特别保留的 attribute,不会作为 props 传递给插槽。因此最终 headerProps 的结果是 { message: 'hello' }。
template
<template>
<MyComponent>
<!-- 使用显式的默认插槽 -->
<template #default="{ message }">
<p>{{ message }}</p>
</template>
</MyComponent>
</template>
5、高级列表组件示例
你可能想问什么样的场景才适合用到作用域插槽,这里我们来看一个 组件的例子。它会渲染一个列表,并同时会封装一些加载远端数据的逻辑、使用数据进行列表渲染、或者是像分页或无限滚动这样更进阶的功能。然而我们希望它能够保留足够的灵活性,将对单个列表元素内容和样式的控制权留给使用它的父组件。我们期望的用法可能是这样的:
template
<FancyList :api-url="url" :per-page="10">
<template #item="{ body, username, likes }">
<div class="item">
<p>{{ body }}</p>
<p>by {{ username }} | {{ likes }} likes</p>
</div>
</template>
</FancyList>
在 <FancyList> 之中,我们可以多次渲染 <slot> 并每次都提供不同的数据 (注意我们这里使用了 v-bind 来传递插槽的 props):
template
<ul>
<li v-for="item in items">
<slot name="item" v-bind="item"></slot>
</li>
</ul>
6、无渲染组件
一些组件可能只包括了逻辑而不需要自己渲染内容,视图输出通过作用域插槽全权交给了消费者组件,这种类型的组件称为无渲染组件。
七、依赖注入
Prop 逐级透传问题,图示:
provide 和 inject 可以解决这一问题。
1、Provide (提供)
为组件后代提供数据,使用到 provide() 函数
接收两个参数,其中注入名,可以是一个字符串或是一个 Symbol,第二个参数是提供的值,值可以是任意类型,包括响应式的状态,比如一个 ref
一个组件可以多次调用 provide(),使用不同的注入名,注入不同的依赖值。
import { ref, provide } from 'vue'
const count = ref(0)
// 注入进来的会是该 ref 对象,而不会自动解包为其内部的值,提供的响应式状态使后代组件可以由此和提供者建立响应式的联系。
provide('key', count)
// 如果不使用 <script setup>,请确保 provide() 是在 setup() 同步调用的:
js
import { provide } from 'vue'
export default {
setup() {
provide(/ 注入名 / 'message', / 值 / 'hello!')
}
}
2、应用层 Provide
除了在一个组件中提供依赖,我们还可以在整个应用层面提供依赖:
js
import { createApp } from 'vue'
const app = createApp({})
app.provide(/ 注入名 / 'message', / 值 / 'hello!')
3、Inject (注入)
要注入上层组件提供的数据,需使用 inject() 函数:
<script setup>
import { inject } from 'vue'
const message = inject('message')
</script>
同样的,如果没有使用
import { inject } from 'vue'
export default {
setup() {
const message = inject('message')
return { message }
}
}
4、注入默认值
如果在注入一个值时不要求必须有提供者,那么我们应该声明一个默认值,和 props 类似:
const value = inject('message', '这是默认值')
在一些场景中,默认值可能需要通过调用一个函数或初始化一个类来取得。为了避免在用不到默认值的情况下进行不必要的计算或产生副作用,我们可以:
使用工厂函数来创建默认值
const value = inject('key', () => new ExpensiveClass())
在一些场景中,可能需要在注入方组件中更改数据,推荐在供给方组件内声明并提供一个更改数据的方法函数:
<!-- 在供给方组件内 -->
<script setup>
import { provide, ref } from 'vue'
const location = ref('North Pole')
function updateLocation() {
location.value = 'South Pole'
}
provide('location', {
location,
updateLocation
})
</script>
<!-- 在注入方组件 -->
<script setup>
import { inject } from 'vue'
const { location, updateLocation } = inject('location')
</script>
<template>
<button @click="updateLocation">{{ location }}</button>
</template>
使用 readonly() 来包装提供的值,确保提供的数据不能被注入方的组件更改
<script setup>
import { ref, provide, readonly } from 'vue'
const count = ref(0)
provide('read-only-count', readonly(count))
</script>
5、使用 Symbol 作注入名
至此,我们已经了解了如何使用字符串作为注入名。但如果你正在构建大型的应用,包含非常多的依赖提供,或者你正在编写提供给其他开发者使用的组件库,建议最好使用 Symbol 来作为注入名以避免潜在的冲突。
我们通常推荐在一个单独的文件中导出这些注入名 Symbol:
js
// keys.js
export const myInjectionKey = Symbol()
js
// 在供给方组件中
import { provide } from 'vue'
import { myInjectionKey } from './keys.js'
provide(myInjectionKey, { /*
要提供的数据
/ });
js
// 注入方组件
import { inject } from 'vue'
import { myInjectionKey } from './keys.js'
const injected = inject(myInjectionKey)
八、 异步组件
1、基本用法
js
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() => {
return new Promise((resolve, reject) => {
// ...从服务器获取组件
resolve(/* 获取到的组件 */)
})
})
// ... 像使用其他一般组件一样使用 `AsyncComp`
ES 模块动态导入也会返回一个 Promise,所以多数情况下我们会将它和 defineAsyncComponent 搭配使用。
类似 Vite 和 Webpack 这样的构建工具也支持此语法 (并且会将它们作为打包时的代码分割点),可以用它来导入 Vue 单文件组件:
js
import { defineAsyncComponent } from 'vue'
异步组件可以使用 app.component() 全局注册:
js
app.component('MyComponent', defineAsyncComponent(() =>
import('./components/MyComponent.vue')
))
也可以直接在父组件中直接定义它们:
vue
<script setup>
import { defineAsyncComponent } from 'vue'
const AdminPage = defineAsyncComponent(() =>
import('./components/AdminPageComponent.vue')
)
</script>
<template>
<AdminPage />
</template>
2、加载与错误状态
js
const AsyncComp = defineAsyncComponent({
// 加载函数
loader: () => import('./Foo.vue'),
// 加载异步组件时使用的组件
loadingComponent: LoadingComponent,
// 展示加载组件前的延迟时间,默认为 200ms
delay: 200,
// 加载失败后展示的组件
errorComponent: ErrorComponent,
// 如果提供了一个 timeout 时间限制,并超时了
// 也会显示这里配置的报错组件,默认值是:Infinity
timeout: 3000
})
3、搭配 Suspense 使用
异步组件可以搭配内置的 <Suspense> 组件一起使用