08.子组件
08.子组件
子组件:在
vue中,可以定义.vue文件,每个文件都是一个子组件,方便拆分和复用。一个Vue组件在使用前需要先被"注册",这样Vue才能在渲染模板时找到其对应的实现全局注册:通过
app.component(name, options)注册,让组件在当前Vue应用中全局可用(直接使用<MyComponent />,不需要额外注册)import { createApp } from 'vue' import App from './App.vue' const app = createApp(App) app.component( // 注册的名字 'MyComponent', // 组件的实现 { /* ... */ } ) // 可以引入组件,链式调用 import ComponentA from './ComponentA.vue' import ComponentB from './ComponentB.vue' import ComponentC from './ComponentC.vue' app .component('ComponentA', ComponentA) .component('ComponentB', ComponentB) .component('ComponentC', ComponentC) app.mount('#app')局部注册:子组件需要在使用它的父组件中显式导入,并且只能在该父组件中使用。它的优点是使组件之间的依赖关系更加明确,并且对
tree-shaking更加友好setup<script setup> // 引入子组件 import ComponentA from './ComponentA.vue' </script> <template> <ComponentA /> </template>options<script> import ComponentA from './ComponentA.js' export default { // 引入子组件 components: { ComponentA }, // data 或 setup 创建响应式数据 setup() { // ... } } </script> <template> <ComponentA /> </template>
8.1 子组件传值
一个组件需要显式声明它所接受的 props,这样 Vue 才能知道外部传入的标签属性哪些是 props,哪些是透传 attribute,我们先讨论传值的情况
- 标签属性是写在子组件标签上的属性,比如通用的
style、class属性,你可以为组件自定义添加一些属性
- 标签属性是写在子组件标签上的属性,比如通用的
声明
props:- 在
setup风格,props可以使用defineProps函数声明 - 在
options风格,props可以使用props选项声明 defineProps传入的参数和props选项一样,两种方法最后都是为组件添加了props属性
- 在
props属性可以接收一个参数名称列表string[]或一个对象- 参数名称列表:例
['foo', 'bar'],在子组件使用时,可以为它添加属性<MyComponent foo="foo" bar="bar" />,属性没有任何的额外检查 - 对象:支持对属性进行运行时类型校验
- 对象的每个键是可接收的参数名称
- 值可以是:
- 一个构造函数,比如
String等内置类型构造函数或自定义类型构造函数 - 一个对象,可以有以下属性
type属性,用于指定属性的类型,类型校验失败会在开发模式下显示警告。类型可以是一个列表,表示该属性可以是其中任意一种类型required属性,用于指定属性是否是必填的default属性,用于指定属性的默认值,对于非Boolean类型,会使用undefined作为默认值。如果是对象类型,default应该是工厂函数,该函数接收组件所接收到的原始props作为参数,返回一个对象作为默认值validator属性,用于自定义属性的校验逻辑,可接收当前值(vue3.4+还支持再接收完整的props参数),如果函数返回false,则表示属性值无效,会显示警告
- 一个构造函数,比如
props属性也可以使用ts对defineProps的返回值类型进行声明,这样依靠ts的静态类型检查,更细致灵活,开发模式下在编译时尽可能转换为对象,生产模式下会进行转换为参数名称列表- 在使用
defineProps()时,参数不可以使用任何来自setup区域的其他变量,在编译时,这个表达式会在外部函数中执行 - 所有在
props定义的值,都是只读的,子组件只能读取值,不能对值进行任何修改操作,也就是单向数据流 - 如果需要修改,需要在子组件内部将
props传入的值作为初始值,转换为ref或computed对象
tsimport { defineProps } from 'vue' const props = defineProps<{ // 基础类型检查 // (给出 `null` 和 `undefined` 值则会跳过任何类型检查) propA: Number, // 多种可能的类型 propB: [String, Number], // 必传,且为 String 类型 propC: { type: String, required: true }, // 必传但可为 null 的字符串 propD: { type: [String, null], required: true }, // Number 类型的默认值 propE: { type: Number, default: 100 }, // 对象类型的默认值 propF: { type: Object, // 对象或数组的默认值 // 必须从一个工厂函数返回。 // 该函数接收组件所接收到的原始 prop 作为参数。 default(rawProps) { return { message: 'hello' } } }, // 自定义类型校验函数 // 在 3.4+ 中完整的 props 作为第二个参数传入 propG: { validator(value, props) { // 必须是以下值之一 return ['success', 'warning', 'danger'].includes(value) } }, // 函数类型的默认值 propH: { type: Function, // 如果需要更细致的类型 // type: Function as PropType<() => string>, // 不像对象或数组的默认,这不是一个 // 工厂函数。这会是一个用来作为默认值的函数 default() { return 'Default function' } }, // 自定义类型,?表示是可选参数,通过 instanceof Person 来检查 propI?: { type: Person, // 或 type: Object as PropType<Person> } }>()jsimport { defineProps } from 'vue' // 提供给defineProps()的参数也可以在options风格中传递给props属性 const props = defineProps(['foo', 'bar']) const props = defineProps({ // 基础类型检查 // (给出 `null` 和 `undefined` 值则会跳过任何类型检查) propA: Number, // 多种可能的类型 propB: [String, Number], // 必传,且为 String 类型 propC: { type: String, required: true }, // 必传但可为 null 的字符串 propD: { type: [String, null], required: true }, // Number 类型的默认值 propE: { type: Number, default: 100 }, // 对象类型的默认值 propF: { type: Object, // 对象或数组的默认值 // 必须从一个工厂函数返回。 // 该函数接收组件所接收到的原始 prop 作为参数。 default(rawProps) { return { message: 'hello' } } }, // 自定义类型校验函数 // 在 3.4+ 中完整的 props 作为第二个参数传入 propG: { validator(value, props) { // 必须是以下值之一 return ['success', 'warning', 'danger'].includes(value) } }, // 函数类型的默认值 propH: { type: Function, // 不像对象或数组的默认,这不是一个 // 工厂函数。这会是一个用来作为默认值的函数 default() { return 'Default function' } }, // 自定义类型,通过 instanceof Person 来检查 propI: { type: Person, } })- 参数名称列表:例
你可以利用
v-bind在组件标签上传递来自父组件的响应值,实现子组件的状态依据父组件响应式更新- 所有使用
v-bind绑定的属性都会在修改时,自动修改传递给子组件的值,即修改对应的props中的数据 - 如果需要在子组件中修改父组件的数据,需要使用事件完成
FatherComponent.vue<template> <ChildComponent :foo="foo" /> <template> <script setup> import ChildComponent from './ChildComponent.vue' const foo = ref('foo')ChildComponent.vue<template> <div>{{ props.foo }}</div> <template> <script setup> import { defineProps } from 'vue' const props = defineProps({ foo: String }) </script>关于属性命名转换
- 因为
HTML中不区分大小写,vue不鼓励直接使用驼峰命名作为标签属性,一般都是使用-连接命名,比如用foo-bar代替fooBar - 子组件标签上的非动态属性会通过
camelize函数自动映射回Prop内部的驼峰命名,符合js的规范 - 对于动态命名,即
:[key]="value"的形式不会自动处理,但可以使用v-bind.camel:[key]修饰符处理
- 所有使用
8.2 组件事件
组件事件是能让子组件向父组件发送消息的一种方式,为了实现
vue的事件注册,你只需要为子组件声明可产生的更新事件- 在
setup风格中,需要使用defineEmits函数完成,这个函数可以传入一个事件名称列表,比如['update:bar'],最终返回一个可用的事件触发emit函数 - 在
options风格中,需要使用emits选项,这个选项也支持传入一个事件名称列表,比如['update:bar'] - 可以使用
ts对defineEmits返回的emit函数提供类型
import { defineEmits } from 'vue' // js 风格 const emit = defineEmits(['update:bar']) // ts 风格 const emit1 = defineEmits<{ (e: 'update:bar', value: string): void }>() // vue3.3+ 也可以 const emit3 = defineEmits<{}>{ 'update:bar': [value: string] } // 运行时判断写法,这种写法也可以用在 options 风格 const emit2 = defineEmits({ submit: (value: string) => { // 允许进行运行时判断,验证参数是否正确 // 返回 true 表示通过,否则输出警告 return true } })- 在
使用
props和emit,vue3实现了组件的双向绑定,你可以为组件添加v-model绑定的变量属性- 对于
v-model,实际上就相当于在子组件上添加了一个v-bind绑定的属性和一个更新事件@update:属性名- 在
vue3.4之前,需要在内部使用emit触发事件修改外部的数据。emit是一种能够让子组件触发外部事件的方法 - 在
vue3.4之后,有一个defineModel便利方法,允许直接在内部修改对应的model,该方法编译后将展开为对应的prop和emit
defineModel可以设置默认值,这可能导致内部外部的初始状态不一致,外部为undefined时,内部为默认值 - 在
FatherComponent.vue<template> <ChildComponent :foo="foo" v-model="bar" /> <!-- 对应编译为 --> <!-- <ChildComponent :foo="foo" :modelValue="bar" @update:modelValue="$event => (bar = $event)" /> --> <template> <script setup> import ChildComponent from './ChildComponent.vue' const foo = ref('foo') const bar = ref('bar')ChildComponent.vue<template> <div>{{ props.foo }}</div> <!-- 方法1 --> <input type="text" :value="props.bar" @input="(e) => emit('update:bar', e.target.value)" /> <!-- 方法2 --> <input type="text" v-model="model" /> <template> <script setup> import { defineModel, defineProps, defineEmits } from 'vue' const props = defineProps({ foo: String, bar: String }) // 对应 options 写法的 emit 选项 const emit = defineEmits(['update:bar']) // defineModel // 第一个参数是属性名,默认是modelValue // 第二个参数是属性的配置,和Prop支持的配置项一样 const model = defineModel('bar', { default: '' }) // 可以直接修改 model.value </script>vue2的子传父通信方式
在
vue2中,子组件修改父组件的数据,也需要利用事件完成,但这个事件名称默认为input,你需要在子组件中触发这个事件,传递需要修改的结果vue2也提供了修改这个事件名的方法,可以使用model选项,设置不同的名称vue2对于多个v-model属性的支持有限,只能设置唯一的v-model属性// 依然使用 v-model 传值 // <ChildComponent v-model="value" /> export default { model: { // 传递给 props.value prop: 'value', // 修改要触发 change 事件 event: 'change' }, props: { // 依然需要注册 value: String, } methods: { change(value) { // 修改外部数据 this.$emit('change', value) } } }
- 对于
处理
v-model的修饰符:对于v-model,有时你会希望自定义和读取修饰符,因此props提供了对v-model修饰符的支持- 如果绑定的属性为
bar,则读取修饰符为barModifiers defineModel也有能读取修饰符的写法- 其他的指令没有这个需求,
props无法读取其他指令的修饰符
import { defineModel, defineProps, defineEmits } from 'vue' const [model, modelModify] = defineModel('bar', { set(value: string) { if (modelModify.trim) { return value.trim() } return value } }) // 早期写法 const Props = defineProps<{ bar: string, barModifiers: { default: () => ({}) } }>() const emit = defineEmits<{ (e: 'update:bar', value: string): void }>() function updateBar(value: string) { if (Props.barModifiers.trim) { value = value.trim() } emit('update:bar', value) }- 如果绑定的属性为
8.3 插槽
插槽使组件能够方便地接收模板内容,让子组件在它们的组件中渲染这些片段
最简单的插槽被称为默认插槽,也就是直接在子组件标签内定义内容,然后在子组件中指定渲染位置
- 渲染的作用域:插槽的内容虽然渲染在了子组件中,但实际上模板内容是在父组件渲染的,只能访问父组件作用域的数据
父组件<template> <ChildComponent> <p>插槽内容 {{message}}</p> </ChildComponent> </template> <script setup> import ChildComponent from './ChildComponent.vue' const message = 'hello world' </script>子组件<template> <!-- 默认插槽,标签中添加的内容会渲染在这里 --> <!-- 渲染为 <p>插槽内容</p> --> <slot></slot> </template>为插槽添加默认内容:有时你会希望插槽传入是可选的,可以直接将默认内容写在
slot标签内。没有传入时将渲染默认内容<template> <slot> <p>默认内容</p> </slot> </template>具名插槽:一个组件可以有多个插槽人口,你可以为插槽设置名称,提供的内容会渲染在对应插槽内
- 内部命名:
<slot name="name"></slot> - 父组件使用:子组件内的顶级的
<template #name></template>,其中的内容会渲染在具名插槽内,其中的#实际上是v-slot:的简写,如果需要动态名称可以使用#[name] - 默认插槽命名为
default - 如果在父组件使用时,子组件内部存在顶级的非
<template>标签,这些内容将插入默认插槽中
父组件<ChildComponent> <template #header> <header>标题</header> </template> <!-- 默认插槽 --> <main>主要内容</main> <div> 广告 </div> <template #footer> <footer>脚注</footer> </template> </ChildComponent>子组件<template> <slot name="header"></slot> <slot>123</slot> <slot name="footer"></slot>条件渲染:有时希望在没有传入内容时,不渲染插槽,可以在子组件通过
v-if="$slot.name"判断是否传入实现- 内部命名:
作用域插槽:插槽可以让父组件自定义嵌入内容,但之前也强调了插槽只能访问父组件作用域的数据。因此,
vue还提供了从子组件给父组件插槽传出数据的方式- 子组件在
slot标签上定义的除了name外的属性会传出给父组件 - 父组件通过
v-slot接收- 如果只使用默认插槽,可以在
ChildComponent标签上添加v-slot="slotData" - 如果使用了具名插槽,并希望传出数据,可以使用
template #name="slotData"></template>,为了避免作用域混乱,此时默认插槽如果需要传值必须使用template #default="slotData"
- 如果只使用默认插槽,可以在
slotData对象包含了除了slot标签上的name属性外的其他属性,支持通过解构赋值直接拆解
父组件<ChildComponent1 v-slot="{user}"> <p>Hello, {{user}}</p> </ChildComponent1> <ChildComponent2> <template #default="{user}"> <p>Hello, {{user}}</p> </template> <template #header="{user}"> <header>{{user}}</header> </template> <template #footer="{user}"> <footer>{{user}}</footer> </template> </ChildComponent2>子组件1<div> <slot user="Li"></slot> </div>子组件2<div> <slot user="wang" v-if="$slots.header"></slot> <slot user="Li"> <p>Hello, {{user}}</p> </slot> <slot user="zhang" v-if="$slots.footer"></slot> </div>- 子组件在
8.4 透传Attribute
没有被显式声明为
props的属性,将作为透传属性,传递给子组件- 最常见的透传属性是
class和style,当子组件是单根元素渲染时,这样的透传属性将自动添加到根元素上 class和style属性的合并见样式穿透部分- 如果传递的是
v-on事件处理器,那么它将和原本绑定在子组件根元素的事件处理器一起触发 - 如果子组件根元素只是另外的组件,那这个属性将继续传递
- 最常见的透传属性是
禁用属性的自动透传:从
vue 3.3开始,可以选择禁止属性自动透传,使用defineOptions方法,配置inheritAttrs: false- 设置完成后,依然可以通过
$attrs获取所有透传属性,然后可以在模板中使用v-bind="$attrs"将属性挂载在需要的元素节点上
defineOptions({ inheritAttrs: false })- 设置完成后,依然可以通过
在
js中访问透传属性:可以使用useAttrs方法获取到透传属性- 访问到的透传属性并不是响应式的,不应该在代码中监测它们的变化
- 如果需要响应式访问,应该使用
props方式传入或在onUpdated钩子中处理
const attrs = useAttrs() // 如果是选项式组件 // attrs 将作为一个 setup 函数参数 export default { setup(props, ctx) { console.log(ctx.attrs) } }有多根节点的子组件的属性透传:有着多个根节点的组件不知道如何处理透传,也就是没有自动透传功能,当透传的属性没有通过
v-bind="$attrs"显式接收时,会触发警告
