前端实习准备 — Vue组件相关

114 阅读9分钟

组件注册 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
异步依赖处理