组件
一、 注册组件
1.1 ❌ 全局注册
- 目标文件:
main.js
; - 语法:
import { createApp } from 'vue' import App from './App.vue' const app = createApp(App) // 全局注册 app.component('组件名字', 需要注册组件) app.mount('#app')
- 缺陷:
- 全局注册,但并没有被使用的组件无法生产打包时被自动移除(也叫
tree-shaking
)。如果,你全局注册了一个组件,即使它并没有被实际使用,它仍然会出现在打包后的JS文件中; - 全局注册在大型项目中使用项目依赖关系变得不那么明确。在父组件中使用子组件时,不太容易定位子组件的实现。和使用过多的全局变量一样,这可能会影响应用长期的可维护性;
- 全局注册,但并没有被使用的组件无法生产打包时被自动移除(也叫
1.2 ✅ 局部注册
- 哪里注册哪里使用;
- 选项式的局部注册和Vue2的局部注册一样,这里就不过多介绍了;
- 在
<script setup>
的单文件中:- 不用注册,导入之后直接使用;
- 示例展示:
<script setup> import ComponentA from './ComponentA.vue' </script> <template> <ComponentA /> </template>
- 如果没有
<script setup>
:- 需要使用
components
选项来显示注册; - 对于每个
components
对象里的属性,它们的 key 名就是注册的组件名,而值就是相应组件的实现; - 示例展示:
import ComponentA from './ComponentA.js' export default { components: { // 使用了ES6对象简写语法 ComponentA }, setup() { // ... } }
- 需要使用
二、 组件通信
2.1 父传子 - props
- 需要在子组件中声明
props
来接收传递的数据的属性,有两种声明props
的方式:- ❌ 字符串数组形式;
- ✅ 对象形式;
2.1.1 声明与使用
- 组合式API:
- 采用
defineProps()
宏来声明接收传递的数据; - 在
JS
中可使用defineProps()
返回的对象来访问声明的自定义属性; - 在 template 中,可直接访问
defineProps()
中声明的自定义属性;
- 采用
- 选项式API:
- 可以提供
props
选项来声明接收传递的数据; - 在
JS
中可使用this.$props
来访问声明的自定义的属性; - 在 template 中,可直接使用
props
中声明的自定义属性;
- 可以提供
2.1.2 ❌ 字符串数组形式
- 组合式API:
<script setup> // 使用 defineProps 宏来声明 // 声明接收父组件传递的属性值:自定义属性 defineProps(['属性名1', '属性名2', ……]) </script>
- 选项式API:
<script> export default { // 使用 props 选项 props: ['属性名1', '属性名2', ……] } </script>
2.1.3 ✅ 对象形式:
- 对象形式声明的
props
,可以对传递的值进行校验,如果传递的值不满足类型要求,会在浏览器控制台抛出警告来提醒使用者; - 对象形式声明的
props
,key
是prop
的名称,值则为约束条件;
- 对象中的属性:
type
:类型,如Number、String、Boolean、Array、Object、Date、Function、Symbol等等
default
:默认值,对象 或 数组 应当使用 工厂函数返回(就是用箭头函数返回默认值);required
:是否必填,布尔值(reqired 和 default 二选一);validator
:自定义校验,函数类型;
组合式API:
- 示例展示:
- 父组件:
<script setup> import Son from '@/components/18-组件通信/父传子.vue' import { ref, reactive, onMounted } from 'vue' let name = ref('才浅') let age = ref(22) let gender = ref('男') const obj = reactive({ title: '目前主要掌握的技术栈', class: 'Vue2、Vue3、JS', post: '初级前端开发工程师' }) let flag = ref(true) </script> <template> <h4>组件通信 - 父传子</h4> <!-- 1.有flag,不传obj,有默认值 --> <Son :name="name" :age="age" :gender="gender" flag></Son> <!-- 2.无flag,传递obj,无默认值 --> <Son :name="name" :age="age" :gender="gender" :obj="obj"></Son> <!-- 3.无flag,不传obj,有默认值 --> <Son :name="name" :age="age" :gender="gender"></Son> <!-- 4.都不传递 --> <Son></Son> </template>
- 子组件:
<script setup> import { ref, reactive, onMounted } from 'vue' // EXPLAIN defineProps() - 声明接收父组件传递的属性值:自定义属性 // EXPLAIN 字符串数组形式 // let propsData = defineProps(['name', 'age', 'gender', 'obj', 'flag']) // EXPLAIN 对象形式 const propsData = defineProps({ // EXPLAIN 一个类型直接写就可以 name: String, // EXPLAIN 两个类型及以上,就需要用到数组包裹起来 age: [Number, String], // age 既可以是数组也可以是字符串 gender: String, flag: Boolean, obj: { typr: Object, // 指明类型 // EXPLAIN required 和 default 二者出现一个(既然是必传,要什么默认值,反之,同理) // required: true, // 是否必传 default: () => ({ title: '喜欢的动漫', class: '完美世界、不良人、一念永恒、吞噬星空、……', post: '废柴' }) } }) // EXPLAIN JS中,需要通过 defineProps() 返回的对象来访问声明的自定义属性 const print = () => { Object.keys(propsData).forEach(item => { console.log(`propsData.${item} = ${propsData[item]}`) }) } const addTechnology = () => { } // EXPLAIN 子组件不能直接修改父组件传递的数据,会在控制台抛出一个警告 const changeNameProp = () => { propsData.name = '废柴' } </script> <template> <!-- template 中使用 父组件传递的数据,直接使用自定义属性名即可 --> <div> <p>姓名:{{ name }}</p> <p>年龄:{{ age }}</p> <p>性别:{{ gender }}</p> </div> <hr> <div> <h4>{{ obj.title }}</h4> <p>{{ obj.class }}</p> <p>{{ obj.post }}</p> </div> <hr> <button @click="print">打印 propsData 中的值</button> <hr> // 鼠标右键点击,修改值 <button @click="addTechnology" @mousedown.right="changeNameProp">增加掌握技术栈 - 子传父</button> </template>
- 效果展示:
- flag,不传obj,有默认值:
- 直接写flag,子组件就显示 true;
- 直接写flag,子组件就显示 true;
- 无flag,传递obj,无默认值:
- 即使有默认值,显示的数据还是传递的数据;
- 不写flag,子组件显示 false;
- 无flag,不传obj,有默认值:
- 都不传递,并且obj没设默认值:
- 除了flag,其他的都是undefined;
- 在控制台,抛出一个关于 obj必传 的警告;
- 除了flag,其他的都是undefined;
- 子组件中的prop是只读的,不能修改值:
- flag,不传obj,有默认值:
- 父组件:
选项式API:
- 父组件:
<script> import ButtonVue from './components/Button.vue'; export default { components: { ButtonVue }, data: () => ({ isError: false, // 主题 isFlat: false, // 阴影 btnText: '普通按钮'// 按钮文本 }) } </script> <template> 主题:<input type="checkbox" v-model="isError"> 阴影:<input type="checkbox" v-model="isFlat"> 按钮文本:<input type="text" v-model="btnText"> <hr> <!-- 父向子传值,可采用属性的方式赋值 --> <ButtonVue :title="btnText" :error="isError" :flat="isFlat" /> </template>
- 子组件:
<script> export default { // 自定义属性选项 props: { title: { type: String, required: true }, error: Boolean, flat: Boolean, tips: { type: String, default: '我是一个普通的按钮' } }, methods: { showPropsData() { // 在选项式 API JS 中,可以通过 this.$props 来访问 props 中的内容 console.log(this.$props) console.log(this.$props.title) console.log(this.$props.error) console.log(this.$props.flat) }, changeErrorProps() { // 不能直接修改 props 的数据,因为是只读的 this.$props.error = !this.$props.error } } } </script> <template> <!-- 在视图模板上,可直接使用 props 中的属性 --> <button :title="tips" :class="{ error, flat }" @click="showPropsData" @mousedown.right="changeErrorProps"> {{ title }} </button> </template> <style> button { border: none; padding: 12px 25px; } .error { background-color: rgb(197, 75, 75); color: white; } .flat { box-shadow: 0 0 10px grey; } </style>
- 🔺 注意:
- 所有
prop
默认都是可选的(可传可不传),除非声明了required: true
(必传); - 除
Boolean
外的为传递的可选prop
将会有一个默认值undefined
; Boolean
类型的为传递prop
将被转换为false
;- 🔺 子组件中
props
中的数据是 只读 的,只能通过 父组件 进行 更改; - 当
props
的检验失败后,Vue会抛出一个控制台警告(在开发模式下); prop
的校验是在组件实例被创建之前:- 在选项式 API 中,实例的属性(比如
data
、computed
等) 将在default
或validator
函数中不可用; - 在组合式 API 中,
defineProps
宏中的参数不可以访问<script setup>
中定义的其他变量,因为在编译时整个表达式都会被移到外部的函数中;
- 在选项式 API 中,实例的属性(比如
- 所有
特别提醒:
- 关于
Boolean
类型转换:- 为了更贴近原生
boolean attributes
的行为,声明为Boolean
类型的props
有特别的类型转换规则:
- 如声明时:
defineProps({ error: Boolean })
- 传递数据时:
<MyComponent error />
:相当于<MyComponent :error="true" />
<MyComponent />
:相当于<MyComponent :error="false" />
2.2 子传父 - 事件
- 有时候,父组件在使用子组件时,子组件如何给父组件传值呢?
- 子组件声明自定义事件;
- 子组件触发自定义事件并进行传值;
- 父组件使用子组件时监听对应的自定义事件,并执行父组件中的函数(获取子组件传递值);
- 1️⃣ 子组件 - 声明组件事件:
- 在 选项式API 中,子组件 可通过
emits
选项来 声明自定义事件; - 在 组合式API 中,子组件 可通过
defineEmits()
宏来 声明自定义事件; - 声明自定义事件的方式:
- 字符串数组式声明自定义事件;
- 对象式声明自定义事件;
- 语法:
- 组合式API:
- 字符串数组:
<script setup> // 为了方便,此处的变量名,在后面语法演示就用emit代替 const 变量名 = defineEmits(['事件1', '事件2', '事件3', ……]) </script>
- 对象形式:
<script setup> // 为了方便,此处的变量名,在后面语法演示就用emit代替 const 变量名 = defineEmits({ 事件1: null, 事件2: (val) => { // return true 表示传递的数据通过校验 // return false 表示传递的参数没有通过校验,控制台会有警告语句 // 也可以自己写一个警告语句 // console.warn('xxx') // 不管通没通过校验,val都会传递给父组件 } }) </script>
- 字符串数组:
- 选项式API:
- 字符串数组:
export default { emits: ['事件1', '事件2', '事件3', ……] }
- 对象形式:
export default { emits: { 事件1: null, 事件2: (val) => { // return true 表示传递的数据通过校验 // return false 表示传递的参数没有通过校验,控制台会有警告语句 // 也可以自己写一个警告语句 // console.warn('xxx') // 不管通没通过校验,val都会传递给父组件 } }
- 字符串数组:
- 组合式API:
- 采用对象形式的好处:
- 采用对象式声明自定义事件,还可以进行校验传递的参数是否符合预期要求;
- 对象式声明自定义事件中,属性名 为 自定义事件名,属性值 则是 是否验证传递的参数:
- 属性值为
null
则不需要验证; - 属性值为 函数 ,参数为传递的数据,函数返回
true
则验证通过,返回false
则验证失败,验证失败可以用警告语句提示开发者;
- 属性值为
- 🔺 注意:
- 无论是
true
还是false
都会继续执行下去,父组件都会获取到传递的值;
- 无论是
- 在 选项式API 中,子组件 可通过
- 2️⃣ 子组件 - 触发组件事件:
- 组合式API:
- 可调用
defineEmits()
宏返回的emit(event, ...args)
函数来触发当前组件自定义的事件; event
:触发事件名,字符串类型;...args
:传递参数,可没有,可多个;
- 可调用
- 选项式API:
- 可通过组件的当前实例
this.$emit(event, ...args)
来触发当前组件自定义的事件;
- 可通过组件的当前实例
- 语法:
- 选项式API:
// 触发自定义的某个事件 this.$emit('在emits选项中定义的事件名', 向父组件传递的值) // 在 template 中写时,this 可以省略
- 组合式API:
const emit = defineEmits({ 自定义事件1: null, 自定义事件2: (val) => { // 对向父组件传递的数据进行校验 } }) const 子组件事件名 = () => { emit('自定义事件', 向父组件传递的数据) }
- 选项式API:
- 组合式API:
- 3️⃣ 父祖件 - 监听子组件自定义事件:
- 使用
v-on:event="callback"
或者@event="callback"
来监听子组件是否触发了该事件;event
:事件名字(camelCase
形式命名的事件,在父组件中可以使用camel-case
形式来监听);callback
:回调函数,如果子组件触发该事件,那么在父组件中执行对应的回调函数,回调函数声明参数可自动接收到触发事件传来的值;
- 语法:
- 组合式API:
<script setup> const 父组件触发事件的事件名 = (val) => { // val 就是子组件向父组件传递的数据 // 父组件就可以进行相关操作了 } </script> <template> <子组件标签 @子组件中自定义事件名="父组件中要触发的事件" /> </template>
- 选项式API:
<script> export default { methods: { 父组件触发事件的事件名(val) { // val 就是子组件向父组件传递的数据 // 父组件就可以进行相关操作了 } } } </script> <template> <子组件标签 @子组件中自定义事件名="父组件中要触发的事件" /> </template>
- 组合式API:
- 使用
- 基于上一小节的代码的示例展示:
- 父组件:
<script setup> import Son from '@/components/18-组件通信/父传子.vue' import {ref, reactive } from 'vue' let name = ref('才浅') let age = ref(22) let gender = ref('男') const obj = reactive({ title: '目前主要掌握的技术栈', species: 'Vue2、Vue3、JS', post: '初级前端开发工程师' }) const addSpecies = (val) => { obj.species += val } let flag = ref(true) </script> <template> <h4>组件通信 - 父传子</h4> <!-- 1.有flag --> <!-- <Son :name="name" :age="age" :gender="gender" flag></Son> --> <!-- 2.无flag,传递obj,无默认值 --> <!-- <Son :name="name" :age="age" :gender="gender" :obj="obj"></Son>--> <!-- 3.无flag,不传obj,有默认值 --> <!-- <Son :name="name" :age="age" :gender="gender"></Son> --> <!-- 4.都不传递 --> <!-- <Son></Son> --> <!-- 5.子组件向父组件传值 --> <Son :name="name" :age="age" :gender="gender" :obj="obj" @change-obj="addSpecies"></Son> </template>
- 子组件:
<script setup> import { ref, reactive } from 'vue' // EXPLAIN defineProps() - 声明接收父组件传递的属性值:自定义属性 // EXPLAIN 字符串数组形式 // let propsData = defineProps(['name', 'age', 'gender', 'obj', 'flag']) // EXPLAIN 对象形式 const propsData = defineProps({ // EXPLAIN 一个类型直接写就可以 name: String, // EXPLAIN 两个类型及以上,就需要用到数组包裹起来 age: [Number, String], // age 既可以是数组也可以是字符串 gender: String, flag: Boolean, obj: { type: Object, // 指明类型 // EXPLAIN required 和 default 二者只能出现一个(既然是必传,要什么默认值;反之同理) required: true, // 是否必传 // default: () => ({ // title: '喜欢的动漫', // class: '完美世界、不良人、一念永恒、吞噬星空、……', // post: '废柴' // }) } }) // TODO 自定义事件 // EXPLAIN 字符串数组形式 // const emit = defineEmits(['changeObj']) // EXPLAIN 对象形式 const emit = defineEmits({ // 不需要对参数进行验证,属性值写 null 即可 // changObj: null, // 对传递的参数进行验证,函数返回true表示验证通过,反之则不通过,不管通不通过,该值都会传给父组件 changeObj: (val) => val.constructor === String }) // EXPLAIN JS中,需要通过 defineProps() 返回的对象来访问声明的自定义属性 const print = () => { Object.keys(propsData).forEach(item => { console.log(`propsData.${item} = ${propsData[item]}`) }) } // TODO 向父组件传递数据 const addTechnology = () => { emit('changeObj', 'TypeScript, React') } // EXPLAIN 子组件不能直接修改父组件传递的数据,会在控制台抛出一个警告 const changeNameProp = () => { propsData.species = '废柴' } </script> <template> <!-- template 中使用 父组件传递的数据,直接使用自定义属性名即可 --> <div> <p>姓名:{{ name }}</p> <p>年龄:{{ age }}</p> <p>性别:{{ gender }}</p> </div> <hr> <div> <h4>{{ obj.title }}</h4> <p>{{ obj.species }}</p> <p>{{ obj.post }}</p> </div> <hr> <button @click="print">打印 propsData 中的值</button> <hr> <button @click="addTechnology" @mousedown.right="changeNameProp">增加掌握技术栈 - 子传父</button> </template>
- 父组件:
- 子传父案例:
- 选项式API:
- 父组件:
<script> import StudentVue from './components/Student.vue'; export default { components: { StudentVue }, data: () => ({ student: { name: 'Jack', age: 18, sex: '男' } }), methods: { // 获取子组件传递值 getNewAge(newAge) { console.log('年龄的新值:' + newAge) this.student.age = newAge }, getNewAgeAndName(newAge, newName) { console.log('年龄的新值:' + newAge) console.log('名字的新值:' + newName) this.student.age = newAge this.student.name = newName }, getNewStudent(stu){ console.log('学生新值:'); console.log(stu); this.student.age = stu.age this.student.name = stu.name this.student.sex = stu.sex } } } </script> <template> {{ student }} <hr> <StudentVue @change-student="getNewStudent" @change-age-and-name="getNewAgeAndName" @change-age="getNewAge" /> </template>
- 子组件:
<script> export default { // 自定义事件选项 - 字符串数组(不能对传递的参数进行校验) // emits: ['changeAge', 'changeAgeAndName', 'changeStudent'] // 自定义事件选项 - 对象形式 emits: { changeAge: null, // 无需验证 changeAgeAndName: null, // 无需验证 changeStudent: stu => { if (stu.age <= 0) { console.warn('年龄不得小于等于0') // false:验证不通过,会有警告语句,父组件依旧可以获取该值 return false } // true:验证通过 return true } }, methods: { emitEventAge() { // 选项式通过 this.$emit 触发自定义事件,并传值 this.$emit('changeAge', 30) } } } </script> <template> <button @click="emitEventAge">更改年龄</button> <br> <br> <button @click="$emit('changeAgeAndName', 10, 'Annie')"> 更改年龄和名字 </button> <br> <br> <button @click="$emit('changeStudent', { age: 40, name: 'Drew', sex: '男' })"> 更改学生(验证通过) </button> <br> <br> <button @click="$emit('changeStudent', { age: -10, name: 'Tom', sex: '男' })"> 更改学生(验证失败) </button> </template>
- 父组件:
- 组合式API:
- 父组件:
<script setup> import { reactive } from 'vue'; import StudentVue from './components/Student.vue'; let student = reactive({ name: 'Jack', age: 18, sex: '男' }) // 获取子组件传递值 function getNewAge(newAge) { console.log('年龄的新值:' + newAge) student.age = newAge } function getNewAgeAndName(newAge, newName) { console.log('年龄的新值:' + newAge) console.log('名字的新值:' + newName) student.age = newAge student.name = newName } function getNewStudent(stu){ console.log('学生新值:'); console.log(stu); student.age = stu.age student.name = stu.name student.sex = stu.sex } </script> <template> {{ student }} <hr> <StudentVue @change-student="getNewStudent" @change-age-and-name="getNewAgeAndName" @change-age="getNewAge" /> </template>
- 子组件:
<script setup> // 自定义事件 - 字符串数组形式 const emit = defineEmits(['changeAge', 'changeAgeAndName', 'changeStudent']) // 自定义事件 - 对象形式 let emit = defineEmits({ changeAge: null, // 无需验证 changeAgeAndName: null, // 无需验证 changeStudent: stu => { if (stu.age <= 0) { console.warn('年龄不得小于等于0') // false:验证不通过,会有警告语句,父组件依旧可以获取该值 return false } // true:验证通过 return true } }) function emitEventAge() { // 选项式通过 this.$emit 触发自定义事件,并传值 emit('changeAge', 30) } </script> <template> <button @click="emitEventAge">更改年龄</button> <br> <br> <button @click="emit('changeAgeAndName', 10, 'Annie')"> 更改年龄和名字 </button> <br> <br> <button @click="emit('changeStudent', { age: 40, name: 'Drew', sex: '男' })"> 更改学生(验证通过) </button> <br> <br> <button @click="emit('changeStudent', { age: -10, name: 'Tom', sex: '男' })"> 更改学生(验证失败) </button> </template>
- 父组件:
- 选项式API:
2.3 组件通信 - 购物车综合案例
三、 透传属性和事件
- Vue3官网-透传;
3.1 如何透传属性和事件
- 父组件在使用子组件的时候,如何“透传属性和事件”给子组件呢?
- 透传属性和事件并没有在子组件中用
props
和emits
声明; - 透传属性和事件最常见的如
click、class、id、style
; - 当 子组件 只有 一个根元素 时,透传属性 和 事件 会 自动添加 到 该根元素 上;如果根元素已有
class
或style
属性,它会自动合并;
- 透传属性和事件并没有在子组件中用
- 示例展示:
- 父组件:
<script setup> import { ref, reactive } from 'vue' import Son1 from '@/components/20-透传属性和事件/子1.vue' const test = () => console.log('透传属性和事件') </script> <template> <!-- 透传的属性 (class、style)并没有在子组件中的 props 中声明 透传的事件 (click) 并没有在子组件中的 emits 中声明 --> <Son1 class="son1" style="background-color: rgb(0, 255, 243)" @click="test" ></Son1> </template>
- 子组件:
<script setup> </script> <template> <div class="son-content" :style="{ border: '4px solid purple', boxShadow: '0 0px 8px 2px grey' }"> <h3>子组件</h3> </div> </template>
- 效果展示:
- 可以看到父组件子组件标签上绑定的属性和事件会出现在子组件中;
- 可以看到父组件子组件标签上绑定的属性和事件会出现在子组件中;
- 父组件:
3.2 如何禁止透传属性和事件
- 当只有 一个根元素 的时候,透传属性和事件 会 自动添加 到 该根元素上;
- 禁止透传属性和事件:
- 添加给子组件;
- 组合式API:
- 在
<script setup>
中,需要一个额外的<script>
默认导出一个对象,来写inheritAttrs: false
选项来禁止;
- 在
- 选项式API:
- 可以在组件选项中设置
inheritAttrs: false
来阻止;
- 可以在组件选项中设置
- 示例展示:
- 基于上一小结的代码;
- 子组件(组合式API):
// TODO 禁止透传属性和事件 <script> export default { // 阻止自动透传给唯一的根组件 inheritAttrs: false } </script> <script setup> import { ref, reactive, onMounted } from 'vue' onMounted(() => {}); </script> <template> <div class="son-content" :style="{ border: '4px solid purple', boxShadow: '0 0px 8px 2px grey' }"> <h3>子组件</h3> </div> </template>
- 子组件(选项式API):
// TODO 禁止透传属性和事件 <script> export default { // 阻止自动透传给唯一的根组件 inheritAttrs: false } </script> <template> <div class="son-content" :style="{ border: '4px solid purple', boxShadow: '0 0px 8px 2px grey' }"> <h3>子组件</h3> </div> </template>
- 效果展示:
3.3 多根元素的透传属性和事件
- 多根节点的组件 并 没有自动“透传属性和事件”,由于
vue
不确定要将“透传属性和事件”绑定到哪里【也就是说,透传属性和事件默认会绑定到唯一的根节点上】,所以我们需要v-bind="$attrs"
来显示绑定,否则将会抛出一个运行警告;
- 注意:
- 绑定对象:子组件中 要 接受透传属性和事件 的元素;
- 绑定元素数量不受限制;
- 绑定该属性之后,阻止透传就不会生效;
- 代码展示:
- 还是基于 7.3.1 中的代码;
- 父组件代码没变;
- 子组件:
<script setup> import { ref, reactive, onMounted } from 'vue' onMounted(() => {}); </script> <template> <button class="chip" :="$attrs"> 普通纸片1 </button> <button class="chip" :="$attrs"> 普通纸片2 </button> <button class="chip"> 普通纸片3 </button> <div class="son-content"> <h3>子组件1</h3> </div> </template> <style scoped> .son-content { margin-top: 10px; border: 4px solid purple; box-shadow: 0 0px 8px 2px grey; } </style>
- 效果展示:
3.4 访问 透传属性和事件
- 组合式API:
- 在
<script setup>
中引入useAttra()
来访问一个组件的 “透传属性和事件”;
- 在
- 选项式API:
- 通过
this.$attrs
来访问 “透传的属性和事件”
- 通过
- 🔺 注意:
- 虽然这里的
attrs
对象总是反映为最新的 “透传属性和事件”,但它并 不是响应式 的(考虑到性能因素),你不能通过侦听器去监听它的变化; - 如果 需要响应式,可以使用
prop
或者也可以使用onUpdated()
使得在每次更新时结合最新的attrs
执行副作用;
- 虽然这里的
- 示例展示:
- 还是基于 7.3.1 的代码,父组件代码没有变;
- 子组件:
- 组合式API:
<script setup> import { ref, reactive, onMounted, useAttrs } from 'vue' // TODO 使用 变量 接收 透传的属性和事件 const attrs = useAttrs() const attrsTest = () => { console.log(attrs) console.log(attrs.value) // attrs 不是响应式的数据,可以进行解构 const { class: className, style, onClick } = attrs console.log(className, style, onClick) } // TODO 执行透传的事件 const transferEvent = attrs.onClick </script> <template> <button class="chip" :="$attrs" @mousedown.right="attrsTest"> 普通纸片1 </button> <button class="chip" :="$attrs"> 普通纸片2 </button> <button class="chip" @click="attrs.onClick"> 执行透传事件 - 1 </button> <button class="chip" @click="transferEvent"> 执行透传事件 - 2 </button> <div class="son-content"> <h3>子组件1</h3> </div> </template> <style scoped> .son-content { margin-top: 10px; border: 4px solid purple; box-shadow: 0 0px 8px 2px grey; } </style>
- 如果没有使用
<script setup>
,attrs
会作为setup()
上下文对象的一个属性暴露:
export default { setup(props, ctx) { // 透传 attribute 被暴露为 ctx.attrs console.log(ctx.attrs) } }
- 如果没有使用
- 组合式API:
四、 插槽
- Vue3官网-插槽;
- 插槽这块和Vue2没有什么区别,更详细的笔记或例子请移步Vue2 - 插槽;
4.1 什么是插槽
- 在封装组件时,可以使用
<slot>
元素把不确定的、希望由用户指定的部分定义为插槽;可以理解为给预留的内容提供占位符; - 插槽也可以提供默认内容,如果组件的使用者没有为插槽提供任何内容,则插槽内的默认内容会生效;
- 如果在封装组件时没有预留任何
<slot>
插槽,用户提供传递一些模板片段内容会失效;
4.2 具名插槽
- 如果在封装组件时需要预留多个插槽节点,则需要为每个
<slot>
插槽指定具体的name
名称,这种带有具体名称的插槽叫做 具名插槽; - 没有指定
name
名称的插槽,会有隐含的名称叫做default
(匿名插槽); - 在
<template>
元素上使用v-slot:slotName
或#slotName
向指定的具名插槽提供内容;
4.3 作用域插槽
- 如何在向插槽提供内容的同时获得子组件域内的数据:
- 在声明插槽时使用属性值的方式来传递子组件的数据,这种带有数据的插槽称为作用域插槽;
- 在
template
元素上使用v-slot:slotNmae="slotProps"
或#slotNmae="slotPros"
的方式来访问插槽传递的数据; - 如果没有使用
template
元素,而是直接在使用子组件中直接给默认插槽提供内容,我们可以在使用该子组件时用v-slot="slotProps"
来接收该插槽传递的数据对象;
- 注意:
slot
插槽上的name
是一个Vue
特别保留的属性,不会在作用域插槽中访问到;
五、 单文件组件CSS功能
- 默认情况下,写在
.vue
组件中的样式会全局生效,很容易造成多个组件之间的样式冲突问题; - 导致组件之间样式冲突的根本原因:
- 单页面应用过程中,所有组件的
DOM
结构,都是基于唯一的index.html
页面进行呈现的;
- 单页面应用过程中,所有组件的
5.1 组件作用域CSS
- 当
<style>
标签带有scoped
属性后:- 它的CSS只会影响当前组件的元素,父组件的样式将不会渗透到子组件中;
- 该组件的所有元素编译后会自带一个特定的属性;
data-v-
+8位随机哈希值
;
- 该
<style scoped>
内的选择器,在编译后会自动添加特定的属性选择器; - 子组件的根节点会同时被父组件的作用域样式和子组件的作用域样式影响,主要是为了让父组件可以从布局的角度出发,调整其子组件根元素的样式;
<!-- 让下方选择器的样式只作用在该组件中,或者子组件的根元素上 --> <!-- 该组件的所有元素及子组件中的根元素会加上固定的属性 --> <!-- 该CSS选择器都会自动添加固定的属选择器 --> <style scoped> /* 选择器 */ </style>
5.2 深度选择器
- 处于
scoped
样式中的选择器如果想要做更【深度】的选择,既,能影响到子组件,可以使用:deep()
这个 伪类; :deep()
解释(我自己这么理解的):.box:deep(h3) {} /* 影响 .box 下的 所有h3标签 */ /* 如果这个h3是直接写在template下面(没有父级容器),则不会受影响 */ :deep(.h3) {} /* 影响当前组件所有类名为 .h3 的元素和子组件中的所有类名为 .h3 元素 */
5.3 CSS中的v-bind()
- 单文件组件的
<style>
标签支持v-bind()
CSS
函数将CSS
的值链接到动态的组件状态(变量); - 这个语法同样也适用于
<script setup>
,且支持JavaScript表达式(需要用引号包裹起来); - 实际的值会被编译成 哈希化 的 CSS自定义属性,因此CSS本身仍是静态的;
- 自定义属性 会通过 内联样式 的方式应用到 组件的某个元素上,并且在 源值变更 的时候 响应式地更新;
- 示例展示:
<script setup> import { ref, reactive, onMounted } from 'vue' const buttonTheme =reactive({ backColor: '#6fbbff', textColor: '#fff' }) </script> <template> <button>普通按钮</button> </template> <style scoped> button { background-color: v-bind('buttonTheme.backColor'); color: v-bind('buttonTheme.textColor'); } </style>
- 效果展示:
六、 依赖注入
- Vue3官网-依赖注入;
- 业务场景:
- 有一个深层的子组件,需要一个较远的祖先组件的部分数据;
- 解决方案:
- 使用
props
沿着组件链逐级传递下去;
- 可以在祖先组件中使用
provide
提供数据,后代组件使用inject
注入数据;
- 使用
6.1 provide
(提供数据)
- 在应用层方面:
- 可通过
app.provide()
为后代提供数据; - 语法:
app.provide('属性名', '传递的数据')
- 示例展示:
- 目标文件:
main.js
// NOTE 从 vue 中导入 createApp 函数 import { createApp } from 'vue' import './style.css' // NOTE 导入根组件 App.vue import App from './App.vue' // NOTE 通过 createApp 函数创建应用实例 let app = createApp(App) // NOTE 应用层main注册,为所有的组件提供一个数据 app.provide('userName', '禁止摆烂_才浅') // NOTE mount 函数,将应用实例渲染在容器元素里面 app.mount('#app')
App.vue
组件:<script setup> import Vue1 from '@/components/22-依赖注入/1.vue' import { ref, reactive, onMounted } from 'vue' </script> <template> <div class="area" style="background-color: purple;"> <h3>App 组件</h3> <Vue1></Vue1> </div> </template> <style scoped> .area { padding: 10px; color: #fff; } </style>
1.vue
组件(App.vue的子组件):<script setup> import Vue2 from './2.vue' import { ref, reactive, onMounted } from 'vue' </script> <template> <div class="area-1" style="background-color: yellow;"> <h3>App的子组件</h3> <Vue2></Vue2> </div> </template> <style scoped> .area-1 { padding: 10px; color: #000; } </style>
2.vue
组件(App.vue的孙子组件):<script setup> import { ref, reactive, onMounted, inject } from 'vue' let userName = inject('userName') </script> <template> <div class="area-2" style="background-color: #00ffe3;"> <h3>App的孙子组件</h3> <h5>应用层【main.js】提供给的数据: <span>{{ userName }}</span></h5> </div> </template> <style scoped> .area-2 { padding: 10px; color: #000; } span { padding: 4px; border: 3px solid red; } </style>
- 目标文件:
- 效果展示:
- 可通过
- 在组件中如何提供:
- 组合式API:
<script setup>
中,可通过provide()
函数来为后代组件提供数据;- 语法:
import { provide } from 'vue' provide('注入数据时的名字', 要提供的数据)
- 示例展示:
- 目标文件:
App.vue
;App.vue
:<script setup> import Vue1 from '@/components/22-依赖注入-组合式API/1.vue' import { ref, computed, provide } from 'vue' // TODO 定义响应式数据 let title = '博客' let subtitle = ref('每天进步一点点') // TODO 定义方法 const changeSubtitle = (val) => { subtitle.value = val } // TODO 使用 provide() 提供数据 provide('title', title) provide('subtitle', subtitle) provide('changeSubtitle', changeSubtitle); </script> <template> <div class="area" style="background-color: purple;"> <h3>App 组件</h3> 标题:<input type="text" v-model="title"><br> 副标题:<input type="text" v-model="subtitle"> <Vue1></Vue1> </div> </template> <style scoped> .area { padding: 10px; color: #fff; } </style>
- 目标文件:
- 运行展示:见 6.2 组合式API
- 选项式API:【推荐使用函数的方式】
- 可以通过
provide
选项为后代提供数据; - 语法:
export default { provide: { return { // 注入非响应式数据 注入数据时的名字: 要提供的数据(this.变量名), // 注入响应式数据 注入数据时的名字: computed(() => this.变量名), // 注入函数 注入数据时的名字: 当前组件的函数名(this.函数名) } } }
- 如果想访问到组件的实例
this
,provide
必须采用函数的方式【不能用箭头函数】,为保证注入方和供给方之间的响应性链接,必须借助组合式API中的computed()
函数提供计算属性,还可以提供修改响应式数据的函数(响应式数据的修改,尽量放在同一组件中,为了好维护); - 🔺 注意:
provide
选项通中通过computed()
提供的响应式的数据,需要设置app.config.unwrapInjectedREf = true
以保证注入会自动解包这个计算属性。这将会在Vue3.3后成为一个默认行为,而我们暂时在此告知此项配置以避免后续升级对代码的破坏性。在3.3后就不要这样做了;
- 示例展示:
- 目标文件:
App.vue
+2.vue
App.vue
:<!-- 选项式API --> <script> import Vue1 from '@/components/22-依赖注入/1.vue' import { computed } from 'vue' export default { components: { Vue1 }, data: () => ({ title: '博客', subtitle: '每天进步一点点' }), methods: { changeSubtitle (val) { this.subtitle = val } }, // NOTE 选项式API中,使用 provide 选项来给后代组件提供数据 // NOTE 但是这种方式无法访问组件的实例 this /* provide: { title: '博客', title: this.title } */ // NOTE 想要访问组件的实例 this,provide必须是函数的形式,并且不能是箭头函数 provide() { return { // NOTE 提供的数据 不是响应式的数据 - 注入方 和 提供方 没有响应式的连接 title: this.title, // NOTE 提供的数据是 响应式的数据 subtitle: computed(() => this.subtitle), // NOTE 提供 修改响应式数据 的函数 changeSubtitle: this.changeSubtitle } } } </script> <template> <div class="area" style="background-color: purple;"> <h3>App 组件</h3> 标题:<input type="text" v-model="title"><br> 副标题:<input type="text" v-model="subtitle"> <Vue1></Vue1> </div> </template> <style scoped> .area { padding: 10px; color: #fff; } </style>
2.vue
:<!-- 选项式API --> <script> export default { inject: ['userName', 'title', 'subtitle', 'changeSubtitle'] } </script> <template> <div class="area-2" style="background-color: #00ffe3;"> <h3>App的孙子组件</h3> <h5>应用层【main.js】提供的数据: <span>{{ userName }}</span></h5> <h5>App.vue 提供的 非响应式 的数据: <span>{{ title }}</span></h5> <!-- <h5>App.vue 提供的 响应式 的数据: <span>{{ subtitle.value }}</span></h5> --> <h5>App.vue 提供的 响应式 的数据: <span>{{ subtitle }}</span></h5> <button @click="changeSubtitle('禁止摸鱼')">修改App组件中subtitle的值</button> </div> </template> <style scoped> .area-2 { padding: 10px; color: #000; } span { padding: 4px; border: 3px solid red; } </style>
- 效果展示:
- 目标文件:
- 可以通过
- 组合式API:
6.2 inject
(注入)
组合式API:
- 访问:
- 可通过
inject()
的返回值来注入祖先组件提供的数据;
- 可通过
- 语法:
import{ inject } from 'vue' // 有祖先组件提供的数据 / 没有祖先组件提供数据【没有祖先组件提供数据的时候会抛出一个警告】 const 变量名 = inject('注入的数据名') // 没有祖先组件提供数据,在当前组件可以设置默认值 const 变量名 = inject('注入的数据名', 默认值) // 默认值可以是任何类型的数据 请将 语法 和 实例 结合阅读
- 如果提供数据的值是一个
ref
,注入进来的会是该ref
对象,和提供方保持响应式链接; - 如果注入的数据并没有在祖先组件中提供,则会抛出一个警告,可在
provide()
的 第二个参数 设置默认值 来解决; - 它们可以在
JS
和 视图模板 中 直接访问; - 示例展示:
2.vue
:<script setup> import { inject, onMounted } from 'vue'; // TODO 使用 inject() 函数的返回值来注入祖先组件提供的数据 const _userName = inject('userName') // 应用层提供的数据 const title = inject('title') // 普通数据 - 非响应式数据 const _subtitle = inject('subtitle') // ref响应式数据 - _subtitle 也是 ref 对象 const changeSubtitle = inject('changeSubtitle') // 函数 const likes = inject('likes') // 祖先组件并没有提供数据,则会抛出一个警告 const anime = inject('anime', '完美世界') // 祖先组件没有提供数据,当前组件可以在 inject() 的第二个参数设置默认值 onMounted(() => { console.log('应用层提供的数据:', _userName) console.log('祖先组件提供的非响应式数据:', title) console.log('祖先组件提供的响应式数据:', _subtitle) console.log('祖先组件提供的函数:', changeSubtitle) console.log('注入的数据,祖先组件并没有提供,就会抛出一个警告:', likes) console.log('祖先组件没有提供 anime 这个数据,当前组件设置在注入的时候默认值:', anime) }); </script> <template> <div class="area-2" style="background-color: #00ffe3;"> <h3>App的孙子组件</h3> <h5>应用层【main.js】提供的数据: <span>{{ _userName }}</span></h5> <h5>App.vue 提供的 非响应式 的数据: <span>{{ title }}</span></h5> <!-- <h5>App.vue 提供的 响应式 的数据: <span>{{ subtitle.value }}</span></h5> --> <h5>App.vue 提供的 响应式 的数据: <span>{{ _subtitle }}</span></h5> <button @click="changeSubtitle('禁止摸鱼')">修改App组件中subtitle的值</button> <h5>注入的数据,祖先组件并没有提供,就会抛出一个警告: <span>{{ `${likes}` }}</span></h5> <h5>祖先组件没有提供 anime 这个数据,当前组件设置再注入的时候默认值: <span>{{ anime }}</span></h5> </div> </template> <style scoped> .area-2 { padding: 10px; color: #000; } span { padding: 4px; border: 3px solid red; } h5 { padding-left: 20px; text-align: left; } </style>
- 运行展示:
选项式API:
- 访问:
- 可通过
inject
选项 来声明需要注入祖先组件提供的数据,它们可以在JS
中直接通过this
来访问,在 视图模板 中也可以 直接访问;
- 可通过
- 语法:
export default { // 注入方法一:字符串数组 inject: ['注入的数据名1', '注入的数据名2', '注入的函数名',……] // 注入方法二:对象 inject: { 新的变量名: { from: '注入的数据名' }, // 当 新的变量名 == 注入的数据名 时,form 选项可以省略 注入的数据名(新的变量名): {}, // 祖先组件没有提供数据,会抛出一个警告 // 解决方法:在当前组件,可以使用 default 选项设置默认值 新的变量名: { from: '注入的数据名', default: 默认值 } } } 请将 语法 和 实例 结合阅读
inject
采用对象的形式来注入祖先组件提供的数据有哪些好处?- 可用本地属性名注入祖先组件提供的数据【如相同时(本地接收名注入名和接收的数据名),
form
选项可省略】; - 如果注入的数据并没有在祖先组件中提供,则会抛出警告,可采用
default
选项设置默认值来解决;
- 可用本地属性名注入祖先组件提供的数据【如相同时(本地接收名注入名和接收的数据名),
- 示例展示:(
2.vue
)<!-- 选项式API --> <script> export default { // TODO 注入数据选项 - 祖先组件提供的数据 // inject: ['userName', 'title', 'subtitle', 'changeSubtitle'], // TODO 采用对象形式注入 inject: { _userName: { from: 'userName' }, title: {}, _subtitle: { from: "subtitle" }, changeSubtitle: {}, // EXPLAIN 如果注入的数据,祖先组件并没有提供数据,就会抛出一个警告 likes: { from: 'xiHa' }, // EXPLAIN 解决:当前组件可以设置默认值 anime: { // 没有组件提供这个 anime 数据 from: 'anime', default: '完美世界' } }, mounted() { console.log('应用层提供的数据', this._userName) console.log('祖先组件提供的数据', this.title, this._subtitle) console.log('祖先组件提供的函数', this.changeSubtitle) console.log('注入的数据,祖先组件并没有提供,就会抛出一个警告', this.likes) console.log('祖先组件没有提供 anime 这个数据,当前组件设置再注入的时候默认值', this.anime) }, } </script> <template> <div class="area-2" style="background-color: #00ffe3;"> <h3>App的孙子组件</h3> <h5>应用层【main.js】提供的数据: <span>{{ _userName }}</span></h5> <h5>App.vue 提供的 非响应式 的数据: <span>{{ title }}</span></h5> <!-- <h5>App.vue 提供的 响应式 的数据: <span>{{ subtitle.value }}</span></h5> --> <h5>App.vue 提供的 响应式 的数据: <span>{{ _subtitle }}</span></h5> <button @click="changeSubtitle('禁止摸鱼')">修改App组件中subtitle的值</button> <h5>注入的数据,祖先组件并没有提供,就会抛出一个警告: <span>{{ `${likes}` }}</span></h5> <h5>祖先组件没有提供 anime 这个数据,当前组件设置再注入的时候默认值: <span>{{ anime }}</span></h5> </div> </template> <style scoped> .area-2 { padding: 10px; color: #000; } span { padding: 4px; border: 3px solid red; } h5 { padding-left: 20px; text-align: left; } </style>
- 运行展示: