组件注册 app.component() import
1. 定义一个组件(其实就是使用setup构建的.vue文件)
2. 组件注册
(1) 全局注册
就是createApp中的component方法
<script setup>
import {createApp} from 'vue'
import componentA from './componentA.vue'
import componentB from './componentB.vue'
import componentC from './componentB.vue'
const app = createApp({})
// 可以链式调用
app.component(组件名称,组件来源)
app
.component('componentA',componentA)
.component('componentB',componentB)
.component('componentC',componentC)
</script>
全局注册的组件可以在该应用的任意组件使用,不需要import
<ComponentA/>
<ComponentB/>
<ComponentC/>
(2)局部注册
局部注册需要在父组件中import,并且只能在该父组件使用,后代不可用,依赖关系明确
<script setup>
import componentA from './componentA.vue'
</script>
<template>
<componentA/>
</template>
3. 动态组件 <componnent>元素+is属性实现
<component :is="componentName"></component>
:is的值可以是组件名、组件对象,用于多个组件切换
defineProps() 父传子
prop是特殊的属性,子组件中使用defineProps([])宏声明传递进来的prop属性。defineProps是仅在setup下可用的编译宏命令,不需要import,defineProps返回一个对象,包含传递给子组件的所有prop(属性)。prop接收任意类型的值。
1. 基本使用
// 子组件 myComponent
<script setup>
defineProps(['propA','propB']) // propA和propB自动暴露给模板(template),因此可以在模板直接使用
</script>
<template>
<span>{{ propA }}</span>
<span>{{ propB }}</span>
</template>
// 父组件
<script>
import myComponent from './myComponent.vue'
const propObject = ref([{id:1,propA:'one',propB:'one'},{id:1,propA:'two',propB:'two'}])
</script>
<myComponent v-for="prop in propObject" :key="prop.id" :propA="prop.propA" :propB="prop.propB"/>
2. 解构props
prop只暴露给模板,如果想进行一些JS处理,需要对props进行解构赋值。defineProps返回的是一个rea对象,包含所有传入的prop属性
<script>
const {propA,propB} = defineProps{['propA','propB']}
// 在watchEffect中使用,监听响应式数据,自动触发响应式依赖
// 上述解构之后,propA就是单纯的常量,不是响应式数据,不会自动触发响应式依赖,这里只会执行一次。但是在vue3.5之后,针对defineProps的解构时,会自动变为props.propA,这是一个响应式数据,自动触发响应式依赖。
watchEffect(()=>{
console.log(propA)
})
// 在watch中使用,watch只能监听响应式对象,响应式数据只能通过一个getter函数return,才能触发响应式依赖
watch(propA,()=>{}) // 错误,这里实际是props.propA
watch(()=>propA,()=>{}) // 正确
</script>
3. props的名字格式
组件推荐使用<component/> 而不是<component></component>
props推荐命名greeting-message 而不是greetingMessage
4. 静态和动态props
const msg=ref('hello')
<myComponent propA="hello"/>
<myComponenr :propA="msg"/>
5. 使用一个对象绑定多个prop
const propObj = {
propA:'A',
propB:'B'
}
// 使用v-bind="obj",就能同时将多个属性绑定在一个组件上
<myComponent v-bind="propObj"/>
6. 单向数据流
prop都是单向绑定的,因父组件更新而变化。不能在子组件中更改prop值,会发出警告
<script>
const props = defineProps(['propA','propB'])
props.propA = 'new' // 警告
// 一般都是基于prop做一些数据处理,而不会直接更改prop
const newData = computed(()=>{
props.propA + 'new'
})
</script>
7. prop校验
type可为原生构造函数:String Number Boolean Object Array Function Symbol Error Date
type也可以是自定义的类或构造函数
<script>
defineProps([{
propA:Number,
propB:[Number,String],
propC:{
type:Number,
required:true // 必传
default:100 // 默认值
},
propD:{
type:Object,
default(rowProps){ // 默认对象,rowProps是原始props
return {name:'muggle'}
}
},
propE:{
validator(value,rowProps){ // 自定义校验
return
}
}
}])
</script>
$emit() defineEmits() 子传父
1. 模板中触发事件
// 子组件触发自定义事件
<button @click="$emit('someEvent')"></button>
// 父组件监听自定义事件,并执行相应的回调函数
<myComponent @someEvent="callback"/>
2. 自定义事件someEvent同样支持事件修饰符.once,但和原生DOM事件不一样,组件触发的自定义事件没有冒泡机制,不能监听平级组件或是嵌套组件。这种需要用到全局事件总线,或使用全局状态管理。
3. 事件参数
<button @click="$emit('someEvent',param)"></button>
<myComponent @someEvent="(value)=>console.log(param) "/>
4. script setup下触发事件
// $emit()不能在script setup下使用
<script setup>
// 与defineProps类似,在setup下使用
// defineEmits返回一个函数
const emit = defineEmits(['eventA','eventB'])
function buttonClick(){
emit('eventA',{param1,param2})
}
</script>
<button @click="buttonClick"></button>
<myComponent @eventA="callback" />
5. 事件校验
<script setup>
const emit = defineEmits({
eventA:null,
eventB:({param1,param2}=>{ return xxx }) // 返回true/false检测传入的参数是否合法
})
function myButton(param1,param2){
emit('eventB',{param1,param2})
}
</script>
defineModel() = defineProps() + defineEmits() vue3.5+ 双向传
1. 与props的区别
props是单向绑定,父组件使用v-bind绑定属性,子组件使用defineProps声明接收的属性,返回一个props对象。子组件对于prop是只读的,不能直接修改prop,只能基于prop进行数据处理。
v-model是双向绑定,父组件使用v-model绑定ref/reactive数据,子组件使用defineModel声明接收的响应式数据,返回一个ref对象。父子组件同步更新,双方均可修改其值。
2. 基本使用
// 父组件
<script>
const msg = ref('muggle')
</script>
<myComponent v-model='msg'/>
<span>{{msg}}</span> // 父子组件同步更新
// 子组件
<script>
const msgModel = defineModel() // 返回一个ref对象,所以原生DOM使用v-model的值可以等于这个
</script>
<input v-model="msgModel"/> // 父子组件同步更新
3. 传递多个v-model
// 父组件
<script>
const first = ref('muggle')
const last = ref('wizar')
</script>
<myComponent v-model:firstName="first" v-model:lastName="last"/> // 父组件指定参数名
<span>{{first}}{{last}}</span>
// 子组件
<script>
// 子组件将参数名传进defineModel中
const firstName = defineModel('firstName',{type:String, required:true}) // 因为defineModel实际声明了一个prop,所以可以用prop的校验方法
const lastName = defineModel('lastName')
</script>
<input type="text" v-model="firstName"/>
<input type="text" v-model="lastName"/>
4. v-model修饰符
通过解构赋值,可以在子组件中访问到父组件v-model添加的修饰符
// 父组件
<script>
const msg = ref('hello')
const name = ref('muggle')
</script>
<myComponent v-model.capitalize="msg"/> // 首字母变大写
<myComponent v-model:myName.capitalize="name"/>
// 子组件
<script>
const {msg,msgModifier} = defineModel()
const {myName,nameModifier} = defineModel("myName")
console.log(msgModifier) // {capitalize:true}
console.log(nameModifier) // {capitalize:true}
</script>
透传Attributes( id, class, style, v-on(@) ) 爷传孙
传给组件的属性,没被组件声明为prop或emit + v-on绑定在组件身上的事件监听器,都叫透传Attributs。常见的有class style id
1. 模板中使用$attrs得到包含所有透传attributes的对象
// 父组件
<myButton class="large" @click="onClick"/> // 原生DOM事件有冒泡,自定义事件没有
// 子组件 ($attrs对象,模板中使用)
// 子组件有多个节点,需使用v-bind="$attrs"指定某一节点透传。$attrs是一个对象,可以在模板中直接使用,包含所有的透传Attributes,v-bind="obj"就是将该对象的所有属性绑定到组件上。其中原生DOM事件在$attrs中为$attr.onclick函数。
<baseButton v-bind="$attrs" class="btn"/> // 与父组件合并,即class="btn large" @click="onclick"
<span></span>
// 子子组件
<script>
defineOption({
inheriAttrs:false // 禁用继承
})
</script>
<button></button> // 深层继承
<span></span>
2. script setup中使用useAttrs()得到包含所有透传attributes的对象
<script setup>
import {useAttrs} from 'vue'
const attr = useAttrs() // 返回对象,包含所有透传attributes
</script>
插槽 slot v-slot(#) 子传父
父组件使用子组件时,往子组件标签内添加的内容,为插槽内容,插槽内容可以是文本、任意HTML元素。
子组件模板中使用一个占位符<slot></slot>,作为插槽内容的出口。
1. 使用方式
// 父组件
<myComponent>
// 插槽内容
<span>Click Me!</span>
Muggle
<span>{{msg}}</span> // 插槽内容可以访问父组件的作用域,相当于正常的标签一样,但实际父子组件的作用域还是相互独立的,不能互相访问。
<otherComponent/>
</myComponent>
// 子组件
<button>
// 插槽出口
<slot>
submit // 定义默认内容,即父组件没有给任何插槽内容时,渲染默认内容
</slot>
</button>
2. 具名插槽-- 不同slot渲染不同内容
template包裹的 v-slot:slotName,#slotName 指定插槽名字
其中slotName也可以是动态参数,即v-slot:[activeName]
父组件使用<template #slotName></template>包裹特定插槽内容
子组件<slot name="slotName"/>指定插槽名字
// 父组件
<myComponent>
<template #header>
// slot1内容
</template>
<template> // 默认插槽,即未指定名字的。会自动定义为#default
// slot2内容
</template>
<template #footer>
// slot3内容
</template>
默认slot // 同时为包裹在template下的,也会送入默认插槽
</myComponent>
// 子组件
<div>
<header>
<slot name="header"/>
</header>
<main>
<slot></slot> // 无指定name,默认插槽内容位置
</main>
<footer>
<slot name="footer"/>
</footer>
</div>
3. 条件插槽 -- 子组件条件渲染slot
结合$slot和v-if在子组件中条件渲染插槽,$slot是包含所有插槽的对象,在模板中可以直接使用。
<div>
<div v-if="$slot.header"> // 若该插槽存在,渲染
<slot name="header"/>
</div>
<div v-if="$slot.default"/> // 若该插槽存在,渲染
<slot/>
</div>
<div v-if="$slot.footer"> // 若该插槽存在,渲染
<slot name="footer"/>
</div>
</div>
4. 作用域插槽 -- 一个slot传递props
v-slot="slotProps" ,父组件接收来自子组件<slot/>传递的props。slotProps是一个对象,存储所有的prop,同样可以使用解构赋值。
与正常的props传递一样,只不过接收使用v-slot="slotProps"而不是defineProps。
// 父组件
<myComponent v-slot="slotprops">
<span>{{slotprops.name}}</span>
<span>{{slotprops.age}}</span>
</myComponent>
// 子组件
<script>
const myName = ref('muggle')
const myAge = ref(23)
</script>
<div>
<slot :name="myName" :age="myAge"></slot>
</div>
5. 具名作用域插槽 -- 不同slot传递不同props
template包裹的 v-slot:slotName="slotProps" #slotName="slotProps" ,指定接收某个插槽的prop
// 父组件
<myComponent>
<template #header="headerProps">
{{headerProps}} // {msg:"header"}
</template>
<template #default="headerProps"> // 默认插槽最好加上显示的#default,避免出错
{{defaultProps}} // {msg:"default"}
</template>
<template #header="footerProps">
{{footerProps}} // {msg:"footer"}
</template>
</myComponent>
// 子组件
<scirpt>
const msg1 = ref("header")
const msg2 = ref("default")
const msg3 = ref("footer")
</scirpt>
<div>
<slot name="header" :msg="msg1"></slot>
<slot :msg="msg2"></slot>
<slot name="footer" :msg="msg3"></slot>
</div>
依赖注入 provide(name,value) inject(name) 全局传
props只能用在下一层子组件,依赖注入可以让任何后代使用
1. 父组件提供依赖 provide('注入名',值)
<script setup>
import {provide} from 'vue'
const msg = ref('muggle')
// 注入名可以是字符串/symbol,值可以是任意类型
provide('name',msg)
</script>
2. 应用层提供依赖 app.provide('注入名',值)
<script setup>
import {createApp} from 'vue'
const app =createApp({})
app.provide('name','muggle')
</script>
3. 子组件注入数据 inject('注入名') 返回值
<script setup>
import {inject} from 'vue'
const myName = inject('name','默认值') // 如果注入的是ref,则返回的就是ref,使得父子组件可以同步更新注入的值。默认值可以自行添加。
</script>
4. 与响应式数据配合
一般响应式数据的声明和变更都应该在提供方完成,如果想要对响应式数据进行处理,可以在提供方声明一个方法函数,然后一并传出即可
// 父组件
<script setup>
import {ref,provide,readonly} from 'vue'
const name = ref('muggle')
function changeName(){
name.value = 'wizar'
}
// readnoly确保该ref对象不能被注入方更改
provide('name',{readonly(name),changeName})
</script>
// 子组件
<script setup>
import {inject} from 'vue'
const {name,changeName} = inject('name')
</script>
<button @click = "changeName">{{name}}</button>
5. symbol作注入名
// key.js
<script setup>
// 一般在单独的js文件配置好所有的symbol,作为唯一的键
export const myInjectionKey = symbol()
</script>
// 提供方
<script setup>
import {provide,ref} from 'vue'
import {myInjectionKey} from './key.js'
const msg = ref('hello')
provide(myInjectionKey,msg)
</script>
// 注入方
<script setup>
import {inject} from 'vue'
import {myInjectionKey} from './key.js'
const message = inject(myInjectionKey)
</script>
异步组件 defineAsyncComponent()
1. defineAsyncComponent()接收一个返回promise的函数,获取组件成功调用resolve(),获取失败调用reject()。defineAsyncComponent()返回一个外包装组件。
<script setup>
import {defineAsyncComponent} from 'vue'
// 得到的就是一个经外层包装过的组件,只有页面需要渲染时才加载内部实际的组件
const AsyncComponent = defineAsyncComponent(()=>{
return new Promise((resolve,reject)=>{
// 若成功获取组件
resolve()
// 若获取失败
reject()
})
})
</script>
2. ES中的动态导入也会返回一个promise,可以结合使用
<script setup>
import {defineAsyncComponent} from 'vue'
const AsyncComponent = defineAsyncComponent(()=>import('./myComponent.vue'))
</script>
3. 异步组件的注册,与正常组件一样
(1)全局注册
<script setup> app.component('myComponent',defineAsyncComponent(()=>import('./myComponent.vue')))
</script>
(2)局部注册
<script setup>
// 父组件直接定义
const AsyncComponent = defineAsyncComponent(()=>import('./myComponent.vue'))
</script>
// 直接使用
<AsyncComponet></AsyncComponet>
4.加载错误时状态(高级选项)-- 传入一个对象进行高级设置
<script setup>
const AsyncComponent = defineAsyncComponent({
// 加载函数
loader: ()=>import('./myComponent.vue'),
// 加载中展示的组件
lodadingComponent:LoaddingComponent
// 200ms延时,再展示内部组件
delay:200
// 加载失败展示的组件
errorComponent:ErrorComponent
// 若超过此时间限制,也直接展示加载中的组件
timeout:300
})
</script>
内置组件(无需注册,直接使用)
1. Transition 过渡和动画
用于一个元素/组件进入或离开DOM树时应用动画,可由以下触发:v-if v-show <component :is="name"></component> 特殊key
<template>
<button @click="show=!show">click</button>
// 当transition里面的元素被插入或移除,vue就会检测是否设置了动画,如果有就执行
<transition>
<span v-if="show">hello</span>
</transition>
</template>
<style>
.v-enter-active,
.v-leave-active {
transition: opacity 0.5s ease;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
}
</style>
2. TransitionGroup
将transitionGroup作为渲染容器,绑定一个tag属性说明它作为什么元素,而在其之下的列表元素每个都要有一个独立的key,一般结合v-for来使用。则过渡动画就会应用在每一个列表元素上
<TransitionGroup name="list" tag="ul">
<li v-for="item in items" :key="item">
{{ item }}
</li>
</TransitionGroup>
3. KeepAlive
(1)用于组件切换时保留组件的状态和数据,因为组件切换之后其实被销毁,重新加载是一个新的组件实例。使用keepAlive包括component缓存状态
<keepAlive>
<component :is="activeComponent"></component>
</keepAlive>
(2)include 和exclude属性指定特定组件缓存
<keepAlive :include="A,B,C">
<component :is="activeComponent"></component>
</keepAlive>
inculde的值:英文逗号分隔的字符串,正则表达式,数组(包含组件名的),此外,其值匹配的是activeComponent的name属性值
(3)最大缓存实例数 max属性
若超过缓存数量,则销毁最久没被缓存的实例,为新的实例腾空间
<keepAlive :max="10">
<component :is="activeComponent"></component>
</keepAlive>
(4)缓存实例的声明周期
被DOM移除但作为keepAlive缓存树的一部分,变为不活跃状态而不是卸载,当其作为缓存树的一部分重新插入DOM时,将重新激活。
缓存组件会调用两个钩子
<script setup>
import { onActivated, onDeactivated } from 'vue'
onActivated(() => {
// 调用时机为首次挂载
// 以及每次从缓存中被重新插入时
})
onDeactivated(() => {
// 在从 DOM 上移除、进入缓存
// 以及组件卸载时调用
})
</script>
4. TelePort
模态框,用于将其渲染在整个VUE应用外层
5. Suspense
异步依赖处理