组件编写原则
- 可以有多个根元素
- 组件名称:
- 注册:使用
MyComponent
作为组件名 - 模板中:使用
<MyComponent>
(推荐) 或<my-component>
- 注册:使用
组件注册
全局注册
app.component()
import { createApp } from 'vue'
import App from './App.vue'
import MyComponent from './MyComponent.vue'
const app = createApp(App)
app.component(
// 注册的名字
'MyComponent',
// 组件的实现
MyComponent
)
局部注册
通过 <script setup>
,导入的组件都在模板中直接可用
<script setup>
import ComponentA from './ComponentA.vue'
</script>
<template>
<ComponentA />
</template>
父子组件通信
父传子:props
父
父组件通过 v-bind: 绑定传入子组件中的值:
<Child v-bind:parentmsg="message"></Child>
- prop 命名在 JS 中是小驼峰(camelCase),HTML 中是短横线(kebab-case)
v-bind="obj"
等同于把 obj 对象里的所有属性传进去- 声明为 Boolean 类型的 props 可以通过简写表示 true
子
子组件通过 defineProps()
宏来声明传入的参数:
<script setup>
const props = defineProps(['parentmsg'])
console.log(props.parentmsg)
</script>
子组件中 props 的类型:
- 字符串数组形式:
const props = defineProps( ['para1', 'para2', 'para3'] )
- 对象形式:(一般用对象)
defineProps({
// 基础类型检查,有 String, Number, Boolean, Array, Object, Function, Date, Symbol
// (给出 `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'
}
}
})
子传父:emit
子
使用场景:
- 子组件
<template>
中直接使用内置的$emit
方法触发事件 - 子组件
<script>
中要使用defineEmits()
返回的函数触发事件(这个返回的函数作用类似 $emit)
<script setup>
const emit = defineEmits(['my-event']) // 子组件通过 defineEmits() 宏来声明它要触发的事件
emit('my-event', $event, 1, 2, 3)
</script>
<template>
<button @click="$emit('my-event', $event, 1, 2, 3)">Click Me</button>
</template>
- 事件命名在 JS 中是小驼峰(camelCase),HTML 中是短横线(kebab-case)
- defineEmits() 宏必须直接放置在
<script setup>
的顶级作用域下使用 - 组件触发的事件没有冒泡机制
事件校验(传入对象):
<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-on: 监听子组件中的事件:
<Child v-on:submit="parentFn"></Child>
父访问子:ref
见 ref
- 如果一个子组件使用的是选项式 API 或没有使用
<script setup>
:父组件对子组件的每一个属性和方法都有完全的访问权 - 使用了
<script setup>
的组件:是默认私有的,父组件无法访问到子组件中的任何东西,除非子组件在其中通过 defineExpose 宏显式暴露:
<script setup>
import { ref } from 'vue'
const a = 1
const b = ref(2)
// 像 defineExpose 这样的编译器宏不需要导入
defineExpose({
a,
b
})
</script>
父组件监听子组件生命周期事件
在子组件上监听 vue 事件:
<script setup>
function onChildMounted() {
console.log('child mounted')
}
</script>
<template>
<child-component @vue:mounted="onChildMounted">
</template>
子访问父:$parent
只能在 template 中使用。同上,也需要父组件使用 defineExpose
暴露变量
v-model
Vue3 取消了 .sync
,取而代之的是 v-model
实现组件双向绑定
3.4 开始,推荐使用 defineModel()
宏:
<!-- Parent.vue -->
<script setup>
import { ref } from 'vue'
const countModel = ref(0)
</script>
<template>
<Child v-model="countModel" />
</template>
<!-- Child.vue -->
<script setup>
const model = defineModel()
function update() {
model.value++
}
</script>
<template>
<div>Parent bound v-model is: {{ model }}</div>
<button @click="update">Increment</button>
</template>
它等价于 3.4 之前的如下写法:
<!-- Parent.vue -->
<script setup>
import { ref } from 'vue'
const countModel = ref(0)
</script>
<template>
<Child
:modelValue="countModel"
@update:modelValue="value => (countModel = value)"
/>
</template>
<!-- Child.vue -->
<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
</script>
<template>
<input
:value="props.modelValue"
@input="emit('update:modelValue', $event.target.value)"
/>
</template>
多个 v-model 绑定
在上面加参数:
<!-- Parent.vue -->
<script setup>
import { ref } from 'vue'
const first = ref('')
const last = ref('')
</script>
<template>
<Child
v-model:first-name="first"
v-model:last-name="last"
/>
</template>
<!-- Child.vue -->
<script setup>
const firstName = defineModel('first-name')
const lastName = defineModel('last-name')
</script>
<template>
<div>Parent bound v-model is: {{ firstName }} {{ lastName }}</div>
<input type="text" v-model="firstName" />
<input type="text" v-model="lastName" />
</template>
跨层级祖孙通信
provide 和 inject
上层组件 provide()
函数提供值:
<script setup>
import { provide } from 'vue'
provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
</script>
- 注入名可以是一个字符串或是一个 Symbol
- 多次调用 provide(),使用不同的注入名,注入不同的依赖值
- 如果提供的值是一个 ref,那么祖孙组件可以进行响应式链接,如果不想下层组件修改值,使用 readonly() 来包装提供的值:
provide('read-only-count', readonly(count))
全局 provide:
import { createApp } from 'vue'
const app = createApp({})
app.provide(/* 注入名 */ 'message', /* 值 */ 'hello!')
下层组件使用 inject()
函数注入值:
<script setup>
import { inject } from 'vue'
const message = inject('message')
</script>
在上层组件没有提供值时,inject 可以有默认值:
const value = inject('message', '这是默认值')
const value = inject('key', () => new ExpensiveClass())
透传
指的是传递给一个组件,却没有被该组件声明为 props 或 emits 的 attribute 或者 v-on 事件监听器
当一个组件以单个根元素渲染时,透传的 属性和事件 会自动被添加到根元素上,多个根节点的组件没有自动透传行为
使用场景:
<template>
中使用:可以通过内置的$attrs
拿到,包含了除组件所声明的 props 和 emits 之外的所有其他 attribute,例如 class,style,v-on 监听器等等<script>
中要使用useAttrs()
访问
<script setup>
import { useAttrs } from 'vue'
const attrs = useAttrs()
</script>
- attrs 对象总是反映为最新的透传 attribute,但它并不是响应式的 (考虑到性能因素),如果需要响应性,可以使用 prop。或者也可以使用 onUpdated() 使得在每次更新时结合最新的 attrs 执行副作用。
- 透传 attributes 在 JavaScript 中保留了它们原始的大小写
- 像 @click 这样的一个 v-on 事件监听器将在此对象下被暴露为一个函数 $attrs.onClick
不想自动继承到根元素?
使用 defineOptions
<script setup>
defineOptions({
inheritAttrs: false
})
</script>
组件通信总结
方式 | 优点 | 缺点 |
---|---|---|
props ,emit | 常用 | 只能父子 |
ref ,emit | 偶尔用 | 父子组件耦合 |
provide ,inject | 跨层级传数据方便 | 不清楚数据来源与变更 |
$attrs | 数据透传 | 中间组件多余代码 |
Pinia | 数据集中管理 | 代码比较复杂 |
EventBus (第三方库:如 mitt 或 tiny-emitter) | 各种不同组件间通信很方便,代码简洁 | 事件多了后,难以对事件进行维护 |
localStorage ,sessionStorage | 简单,浏览器自带 | 数据和状态比较混乱,不容易维护 |
动态组件
在 component 内置组件上使用 is 来切换不同的组件:<component v-bind:is="xxx"></component>
如果在切换的时候想保存之前组件的状态:使用 keep-alive
<KeepAlive>
<component v-bind:is="xxx"></component>
</KeepAlive>
异步组件
defineAsyncComponent
结合 import()
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() =>
import('./components/MyComponent.vue')
)
最后得到的 AsyncComp 是一个外层包装过的组件,仅在页面需要它渲染时才会调用加载内部实际组件的函数,可以实现延迟加载