组件化开发是学习这些前端框架最核心的知识,在开发页面的时候,可以先按照页面结构进行组件化拆分
例如:

Props
Props 是组件的输入,用于接收父组件传递的数据,是父子组件通信最基础的方式。
- 核心特性
- 单向数据流:父组件数据更新 → 子组件 props 自动更新,但子组件不能直接修改 props(否则会触发警告)。
- 类型校验:可定义 props 的类型、默认值、必填项,提升组件健壮性。
子组件
<script setup lang="ts">
const props = defineProps({
msg: {
type: String,
required: true,
default: "", // 非必填时生效
},
count: {
type: Number,
default: 0
},
// 复杂类型(默认值需用函数返回,避免引用类型共享)
user: {
type: Object,
default: () => ({name: '默认用户', age: 18})
},
// 自定义校验
status: {
type: String,
validator: (value) => {
return ['success', 'error', 'warning'].includes(value)
}
}
})
</script>
<template>
<div>子组件值: {{ props }}</div>
</template>
<style scoped>
</style>
父组件
<script setup lang="ts">
import Com from "@/components/com.vue";
const user = {
name: "张三",
age: 18,
}
</script>
<template>
<div>
<com msg="xxx" :count="1" :user="user" status="warning"></com>
</div>
</template>
如果使用ts,子组件定义props会简单一点
<script setup lang="ts">
interface userType {
name: string
age: number
}
interface Props {
msg: string
count?: number
user?: userType
status?: "success" | "error" | "warning"
}
const props = defineProps<Props>()
const {count = 0} = props
</script>
如果需要设置默认值,可以通过对象解构的方式,但是会丧失响应式
也可以使用withDefaults
const props = withDefaults(defineProps<Props>(), {
age: 18
})
Emits
在子组件中,做了某些操作之后,应该需要通知父组件
这个时候,我们需要用到emits
子组件
<script setup lang="ts">
const emits = defineEmits(["rev-data", "ok"])
function handler(e: InputEvent){
const val = (e.target as HTMLInputElement).value
emits("rev-data", val)
}
function okHandler(){
emits("ok")
}
</script>
<template>
<div>
<input placeholder="输入内容" @keyup.enter="handler">
<button @click="okHandler">完成</button>
</div>
</template>
<style scoped>
</style>
父组件
<script setup lang="ts">
import Emit from "@/components/emit.vue";
function revData(value: string){
console.log("子组件内容", value)
}
function ok(){
console.log("子组件完成")
}
</script>
<template>
<div>
<emit @rev-data="revData" @ok="ok"></emit>
</div>
</template>
<style scoped>
</style>
如果要增加校验规则,需要以对象的形式声明
<script setup lang="ts">
const emits = defineEmits({
// 自定义提交事件:校验参数必须包含 username 且非空
submit: (data) => {
if (data && data.username && data.username.trim()) {
return true; // 校验通过
} else {
console.warn('提交失败:用户名不能为空');
return false; // 校验失败
}
}
});
function handler(e: Event){
const val = (e.target as HTMLInputElement).value
emits("submit", {username: val})
}
</script>
<template>
<div>
<input placeholder="用户名" @keyup.enter="handler">
</div>
</template>
<style scoped>
</style>
注意,校验失败父组件也是可以拿到数据的

ts对emits进行类型声明
<script setup lang="ts">
const emits = defineEmits<{
(e: "submit", val: string):void
(e: "ok"):void
}>()
function handler(e: Event){
const val = (e.target as HTMLInputElement).value
emits("submit", val)
}
function ok(){
emits("ok")
}
</script>
<template>
<div>
<input placeholder="输入内容" @keyup.enter="handler"> <button @click="ok">ok</button>
</div>
</template>
<style scoped>
</style>
双向绑定组件
如果我想给自己的组件实现v-model的双向绑定功能
那么需要用特定的emits,例如
v-model单个值
子组件
<script setup lang="ts">
interface Props {
modelValue: string
}
const props = defineProps<Props>()
const emits = defineEmits<{
(e: "update:modelValue", val: string): void
}>()
function handler(e: Event) {
const val = (e.target as HTMLInputElement).value
emits("update:modelValue", val)
}
</script>
<template>
<div>
<input @input="handler" :value="props.modelValue" placeholder="输入内容">
<br>
输入的内容: {{ props.modelValue }}
</div>
</template>
父组件
<script setup lang="ts">
import Shuang1 from "@/components/shuang1.vue";
import {ref} from "vue";
const text = ref("")
</script>
<template>
<div>
<shuang1 v-model="text"></shuang1>
<br>
text的值: {{ text }}
</div>
</template>
如果是v-model="xxx",那么在子组件里面,props对应的名字就是modelValue,emits对应的事件名称就是update:modelValue,这个是固定的
v-model多个值的情况
这个还简单一点,把modelValue替换成需要改的值就行,例如:
子组件
<script setup lang="ts">
interface Props {
visible: boolean
age: number
}
const props = defineProps<Props>()
const emits = defineEmits<{
(e: "update:visible", visible: boolean): void
(e: "update:age", like: number): void
}>()
function visibleHandler(e: Event) {
const val = (e.target as HTMLInputElement).value
if (val === "true"){
emits("update:visible", true)
return
}
emits("update:visible", false)
}
function ageHandler(e: Event) {
const val = (e.target as HTMLInputElement).value
emits("update:age", Number(val))
}
</script>
<template>
<div>
修改visible
<select :value="visible" @input="visibleHandler">
<option :value="true">成功</option>
<option :value="false">取消</option>
</select>
<br>
修改age
<input type="number" placeholder="输入年龄" @input="ageHandler" :value="props.age">
<br>
输入的内容: {{ props }}
</div>
</template>
<style scoped>
</style>
父组件
<script setup lang="ts">
import {ref} from "vue";
import Shuang2 from "@/components/shuang2.vue";
const visible = ref(false)
const age = ref(0)
</script>
<template>
<div>
<shuang2 v-model:visible="visible" v-model:age="age"></shuang2>
</div>
</template>
组件通信
Vue3 中组件通信是开发的核心知识点,根据组件间的关系(父子、隔代、兄弟、任意组件),有不同的通信方案
父传子
通常情况下父组件通过属性传递数据,子组件通过 defineProps接收。
特点:Props 是单向数据流(父→子),子组件不能直接修改 Props,需通过事件通知父组件修改。
但是有特殊的情况,父组件通过ref组件对象,调用子组件抛出来的方法,父组件也能传递数据给子组件,一般用于运行时的数据更新(在生命周期中会讲到)
子传父
子组件通过 defineEmits 声明事件,调用 emit 触发;父组件通过 @事件名 监听。
还有一种使用props的特殊情况,父组件传递属性的时候,传递一个函数,子组件可以调用这个函数,不常用,了解即可
父子双向绑定v-model
Vue3 支持自定义组件的 v-model,默认绑定 modelValue 属性和 update:modelValue 事件,也可自定义参数。
隔代组件通信
透传
中间组件直接把props和emits透传给目标组件,例如:
父组件
<script setup lang="ts">
import Middle1 from "@/components/middle1.vue";
function emit(name: string){
alert(name)
}
</script>
<template>
<div>
App
<middle1 name="fengfeng" @emit="emit"></middle1>
</div>
</template>
中间组件
<script setup lang="ts">
import Sub1 from "@/components/sub1.vue";
interface Props {
name: string
}
const props = defineProps<Props>()
const emits = defineEmits(["emit"])
function emit(name: string, x: string){
// 中间组件也可以包装一下
console.log(x)
emits("emit", name)
}
</script>
<template>
<div>
中间组件
<!-- <sub1 :name="props.name" @emit="emits('emit', $event)"></sub1>-->
<sub1 :name="props.name" @emit="emit($event, 'xxx')"></sub1>
</div>
</template>
<style scoped>
</style>
子组件
<script setup lang="ts">
interface Props {
name: string
}
const props = defineProps<Props>()
const emits = defineEmits(["emit"])
function emit(name: string){
emits("emit", name)
}
</script>
<template>
<div>
孙子组件
name: {{ props.name }}
<button @click="emit('sun')">告诉App组件</button>
</div>
</template>
<style scoped>
</style>
Provide和Inject
- 核心逻辑:父组件(祖先)通过
provide提供数据,任意后代组件通过inject注入使用。 - 特点:无视层级,适合全局共享(如主题、用户信息);默认非响应式,需传递
ref/reactive保持响应式。
父组件使用provide传递数据
import S3 from "@/components/s3.vue";
import {provide} from "vue";
provide("msg", "abc")
子组件从inject中接收数据
<script setup lang="ts">
import {inject} from "vue";
const msg = inject("msg")
</script>
<template>
<div>
msg: {{ msg }}
</div>
</template>
父组件可以传任何数据给provide,如果这个数据是响应式的,那么子组件还可以直接修改它,虽然不建议这么做
如果不想让子组件改,可以套一个readonly
父组件
<script setup lang="ts">
import {ref} from "vue";
import S3 from "@/components/s3.vue";
import {provide, readonly} from "vue";
const msg = ref("")
const readonlyMsg = ref("")
provide("msg", msg)
provide("readonlyMsg", readonly(readonlyMsg))
</script>
<template>
<div>
<s3></s3>
msg: {{ msg }}
readonlyMsg: {{ readonlyMsg }}
</div>
</template>
子组件
<script setup lang="ts">
import {inject} from "vue";
const msg = inject("msg")
const readonlyMsg = inject("readonlyMsg")
</script>
<template>
<div>
msg: <input v-model="msg">
readonlyMsg: <input v-model="readonlyMsg">
</div>
</template>
mitt
Vue3 移除了 $on/$off,需手动实现 EventBus(基于第三方库如 mitt 或原生 EventEmitter),适合简单的跨层级通信。
npm i mitt
创建bus实例
import mitt from 'mitt'
const bus = mitt()
export default bus
发送方组件
<script setup lang="ts">
import S4 from "@/components/s4.vue";
import bus from '@/utils/bus'
const sendMsg = () => {
bus.emit('custom-event', '跨层级消息') // 只能传一个参数
}
</script>
<template>
<div>
<button @click="sendMsg">点我</button>
<s4></s4>
</div>
</template>
接收方组件
<script setup lang="ts">
import {onMounted, onUnmounted} from 'vue'
import bus from '@/utils/bus'
onMounted(() => {
// 监听事件
bus.on('custom-event', (msg) => {
console.log(msg)
})
})
onUnmounted(() => {
// 取消监听(防止内存泄漏)
bus.off('custom-event')
})
</script>
兄弟组件
- 方式1:父组件中转(子1→父→子2,基础方案)
- 方式2:使用mitt
- 方式3:Pinia状态管理 讲pinia的时候会讲到
- 方式4:通过路由 讲路由的时候会讲到
方式一:
通过父组件中转,假如子1向父提交一个事件,在父组件中处理这个事件,然后通过属性的方式传递给子2

方式二:
通过mitt的方式,b1直接向bus发送一个事件消息,在b2中监听这个事件即可

组件插槽
在定义组件时,有些内容还不能确定,那就可以先使用插槽占个位
默认插槽
<script setup lang="ts">
</script>
<template>
<div>
<div>1</div>
<!-- 插槽默认值:如果使用组件时没传内容,就显示这个 -->
<slot>如果父组件没有占位,就默认显示这个</slot>
<div>3</div>
</div>
</template>
父组件
<script setup lang="ts">
import Slot1 from "@/components/slot1.vue";
</script>
<template>
<div>
<slot1></slot1>
<slot1>父组件占位</slot1>
</div>
</template>
具名插槽
<script setup lang="ts">
</script>
<template>
<div>
<div class="head">
<slot name="head"></slot>
</div>
<div class="body">
<!--使用默认插槽-->
<slot></slot>
</div>
<div class="footer">
<!--通过name给插槽起个名字-->
<slot name="footer"></slot>
</div>
</div>
</template>
<style scoped>
div{
color: #2c3e50;
}
.head{
background-color: #ee4a4a;
padding: 20px;
}
.body{
background-color: bisque;
padding: 20px;
}
.footer{
background-color: salmon;
padding: 20px;
}
</style>
父组件使用
<script setup lang="ts">
import Slot2 from "@/components/slot2.vue";
</script>
<template>
<div>
<slot2>单独替换默认插槽</slot2>
<slot2>
<template #default>默认插槽</template>
<template #footer>footer</template>
<template v-slot:head>head</template>
</slot2>
</div>
</template>
作用域插槽
父组件填充插槽内容时,需要用到子组件内部的数据(比如子组件的列表数据,父组件想自定义渲染方式)
子组件通过 v-bind 给插槽传递数据(“作用域”),父组件通过 v-slot 接收这些数据
<script setup lang="ts">
import {ref} from "vue";
const list = ref([
{name: "张三", age: 13},
{name: "王五", age: 12},
])
</script>
<template>
<div>
<div v-for="item in list">
<slot :data="item">
<span class="name">name: {{ item.name }}</span>
<span>age: {{ item.age }}</span>
</slot>
</div>
</div>
</template>
<style scoped>
.name{
color: #2c3e50;
}
</style>
父组件
<script setup lang="ts">
import Slot2 from "@/components/slot2.vue";
import Slot3 from "@/components/slot3.vue";
</script>
<template>
<div>
<slot3></slot3>
<slot3>
<template #default="slotData">
<span class="name">{{ slotData.data.name }}</span>
<span class="age">{{ slotData.data.age }}</span>
</template>
</slot3>
</div>
</template>
<style scoped>
.name{
color: red;
}
</style>
需要注意的是,使用scope的情况下,如果父组件替换了插槽,在不使用样式穿透的情况下,这个样式只能由父组件修改
组件生命周期
组件生命周期指的是 Vue 组件从创建到挂载、更新、卸载的整个过程,Vue 提供了一系列 “生命周期钩子函数”,让你能在特定阶段执行自定义逻辑
Vue3 组件生命周期主要分为 4 大阶段:
- 创建阶段:组件实例初始化,尚未挂载到 DOM
- 挂载阶段:组件实例挂载到 DOM 树,页面可见
- 更新阶段:组件响应式数据变化,触发视图更新
- 卸载阶段:组件从 DOM 树移除,实例销毁
| 钩子函数 | 对应阶段 | 触发时机 | 常用场景 |
|---|---|---|---|
setup() |
创建阶段 | 组件创建时最先执行(替代 Vue2 的 beforeCreate/created) |
初始化数据、声明响应式变量、定义方法、请求初始数据(注意:此时 DOM 未挂载) |
onBeforeMount() |
挂载前 | 模板编译完成,即将挂载到 DOM 之前 | 最后一次修改数据(未渲染到页面) |
onMounted() |
挂载完成 | 组件挂载到 DOM 树后(可访问 DOM 元素) | 操作 DOM、发起异步请求(如接口数据获取)、初始化第三方插件(如 ECharts) |
onBeforeUpdate() |
更新前 | 响应式数据变化,虚拟 DOM 重新渲染之前 | 获取更新前的 DOM 状态、阻止不必要的更新 |
onUpdated() |
更新完成 | 虚拟 DOM 重新渲染并应用到 DOM 后 | 操作更新后的 DOM(如重新计算元素尺寸) |
onBeforeUnmount() |
卸载前 | 组件实例即将被卸载之前 | 清除定时器、取消事件监听、销毁第三方插件实例(防止内存泄漏) |
onUnmounted() |
卸载完成 | 组件实例已被卸载,DOM 已移除 | 最终清理工作(如清空全局变量、取消接口请求) |
onErrorCaptured() |
错误捕获 | 后代组件抛出错误时触发 | 捕获并处理组件错误,避免页面崩溃 |
onActivated()/onDeactivated() |
缓存组件专属 | 被 <keep-alive> 缓存的组件激活 / 失活时触发 |
缓存组件的状态恢复 / 重置 |
<script setup lang="ts">
import {onBeforeMount, ref} from "vue"; // 挂载前
import {onMounted} from "vue"; // 挂载完成
import {onBeforeUpdate} from "vue"; // 更新前
import {onUpdated} from "vue"; // 更新完成
import {onBeforeUnmount} from "vue"; // 卸载前
import {onUnmounted} from "vue"; // 卸载完成
import {nextTick} from "vue";
const number = ref(0)
const numberRef = ref()
const timer = setInterval(()=>{
console.log(new Date().toLocaleString())
}, 1000)
onBeforeMount(()=>{
console.log("onBeforeMount")
})
onMounted(()=>{
console.log("onMounted")
})
onBeforeUpdate(()=>{
// 还没有更新到dom上,所以获取dom是之前的数据
// numberRef 直接使用ref获取
nextTick(()=>{
console.log("nextTick number", numberRef.value.innerHTML)
}) // 如果dom发生更新,会回调这个nextTick
console.log("onBeforeUpdate number", numberRef.value.innerHTML)
})
onUpdated(()=>{
console.log("onUpdated number", numberRef.value.innerHTML)
})
onBeforeUnmount(()=>{
console.log("onBeforeUnmount")
})
onUnmounted(()=>{
console.log("onUnmounted")
clearInterval(timer) // 组件销毁的时候,要把定时器这些清除掉
})
function updateHandler(){
number.value ++
}
</script>
<template>
<div>
<button @click="updateHandler">修改数据</button>
<div>
当前number: <span class="number" ref="numberRef">{{ number }}</span>
</div>
</div>
</template>
<style scoped>
</style>
父组件
<script setup lang="ts">
import Smzq from "@/components/smzq.vue";
import {ref} from "vue";
const visible = ref(false)
</script>
<template>
<div>
<button @click="visible=true">组件创建</button>
<button @click="visible=false">组件销毁</button>
<div>
<smzq v-if="visible"></smzq>
</div>
</div>
</template>
这里引入了两个新知识
ref获取DOM元素
DOM 元素仅在组件挂载后(mounted)可用
ref单个dom元素
<template>
<!-- 模板中的 ref 名称 -->
<div ref="myDom">我是目标DOM</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
// 脚本中声明同名 ref 变量
const myDom = ref(null)
onMounted(() => {
// 仅在挂载后能获取到 DOM 元素
console.log(myDom.value) // <div>我是目标DOM</div>
})
</script>
ref多个dom数据
<template>
<div>
<span v-for="item in 3" ref="spanList">{{ item }}</span>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
// 脚本中声明同名 ref 变量
const spanList = ref([])
onMounted(() => {
// 仅在挂载后能获取到 DOM 元素
console.log(spanList.value) // Proxy(Array) {0: span, 1: span, 2: span}
console.log(spanList.value[0].innerText) // 1
})
</script>
不过这个需要注意一个点,就是手动用同名的ref,只能获取到最后的那个dom元素
<template>
<div>
<span ref="spanList">1</span>
<span ref="spanList">2</span>
<span ref="spanList">3</span>
<span ref="sList" v-for="item in 3">{{ item }}</span>
</div>
</template>
<script setup>
import {ref, onMounted} from 'vue'
// 脚本中声明同名 ref 变量
const spanList = ref([])
const sList = ref([])
onMounted(() => {
// 仅在挂载后能获取到 DOM 元素
console.log(spanList.value) // 最后一个span 3
console.log(sList.value) // 列表
})
</script>
ref组件
子组件需要通过defineExpose抛出方法,父组件才能调用子组件的方法
<script setup lang="ts">
import {ref} from "vue";
const visible = ref(false)
const data = ref("")
function show() {
visible.value = true
}
function setData(msg: string) {
data.value = msg
}
function hide() {
visible.value = false
}
defineExpose({
show,
setData,
hide,
}) // 只有在defineExpose里面的方法,父组件才能用
</script>
<template>
<div>
<div v-if="visible">通过show方法显示 data: {{ data }}</div>
</div>
</template>
<template>
<div>
<div>
<button @click="ref1.show">show</button>
<button @click="ref1.hide">hide</button>
<button @click="ref1.setData('数据')">设置数据</button>
</div>
<ref1 ref="ref1"></ref1>
</div>
</template>
<script setup lang="ts">
import Ref1 from "@/components/ref1.vue";
import {onMounted, ref} from "vue";
const ref1 = ref(null)
onMounted(()=>{
console.log("ref1", ref1.value)
})
</script>
nextTick
nextTick 是 Vue 提供的一个异步方法,用于在 Vue 完成 DOM 更新后执行指定的回调函数。
- 当你修改响应式数据(如 ref/reactive)时,Vue 不会立刻更新 DOM,而是将更新操作缓存到一个 "更新队列" 中;
- 等当前同步代码执行完毕后,Vue 才会批量处理更新队列,统一更新 DOM;
nextTick的作用就是 "插队" 到 DOM 更新完成后的第一个时机执行回调,确保你能获取到最新的 DOM 状态。
<script setup lang="ts">
import {nextTick, ref} from "vue";
const number = ref(0)
const spanRef = ref()
function numberPlus(){
number.value ++
console.log("old spanRef", spanRef.value.innerText)
nextTick(()=>{
console.log("new spanRef", spanRef.value.innerText)
})
}
</script>
<template>
<div>
<button @click="numberPlus">number++</button>
number: <span ref="spanRef">{{ number }}</span>
</div>
</template>
自定义组合式函数
组合式函数(Composables)是基于 Vue3 的组合式 API(ref、reactive、watch、onMounted 等)封装的可复用逻辑函数,命名通常以 use 开头(如 useUser、useLocalStorage)
自定义组合式函数(Composables)是 Vue3 基于组合式 API(Composition API) 封装的、可复用的逻辑函数,本质是提取组件中通用的业务逻辑,封装成独立函数,让代码可以跨组件复用,同时让组件代码更简洁、逻辑更清晰。
你可以把它理解为:专门存放 “可复用逻辑” 的 “工具函数”,但和普通工具函数不同的是,它可以直接使用 Vue 的响应式 API(如 ref、reactive、watch、生命周期钩子等),和组件的上下文深度绑定。
命名规范:通常以 use 开头(如 useCounter、useRequest),这是 Vue 社区的通用约定,一眼就能识别是组合式函数。
需求:多个组件需要 “计数、加 1(increment )、减 1(decrement )、重置” 的逻辑,封装成 useCounter。
// 计数、加 1(increment )、减 1(decrement )、重置
import {ref} from "vue";
export function useCounter(initValue = 0) {
const count = ref(initValue)
function increment() {
count.value++
}
function decrement() {
count.value--
}
function reset(){
count.value = initValue
}
return {
count,
increment,
decrement,
reset,
}
}
使用
<script setup lang="ts">
import {useCounter} from "@/use_tools/useCounter.ts";
const {
count, reset, increment, decrement
} = useCounter(10)
</script>
<template>
<div>
<div>
<button @click="increment">加1</button>
<button @click="decrement">减1</button>
<button @click="reset">重置</button>
</div>
<div>
number: {{ count }}
</div>
</div>
</template>
vue3内置组件
component 动态组件
<component> 是 Vue 提供的内置通用组件容器,核心功能是动态渲染不同的组件—— 你可以通过绑定 is 属性,让它根据不同的值渲染对应的组件,替代大量的 v-if/v-else 来切换组件,让代码更简洁、灵活。
通俗理解:<component> 就像一个 “组件占位符”,你告诉它要渲染哪个组件(通过 is 属性),它就会动态加载并显示那个组件,切换 is 的值就能无缝切换显示的组件。
<script setup lang="ts">
import {computed, ref} from "vue";
import C1 from "@/components/c1.vue";
import C2 from "@/components/c2.vue";
const visible = ref(false)
const conObj = computed(()=>{
return visible.value ? C1 : C2
})
</script>
<template>
<div>
component 动态组件
<div>
<button @click="visible = !visible">切换动态组件</button>
</div>
<div>
<c1 v-if="visible"></c1>
<c2 v-else></c2>
</div>
<div>
<component :is="conObj"></component>
</div>
</div>
</template>
keep-alive缓存组件
keep-alive 是 Vue 内置的抽象组件(不会渲染成 DOM 节点),包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。
- 适用场景:列表页跳详情页返回、多标签页切换、需要保留表单输入状态的场景等。
- 核心特性:
- 缓存组件实例,避免重复创建 / 销毁
- 触发专属生命周期钩子(
activated/deactivated) - 可通过属性控制缓存范围
比如下面这种情况
<script setup lang="ts">
import {ref} from "vue";
const number = ref(0)
</script>
<template>
<div>
我是c3组件
<br>
<button @click="number++">number++</button>
<br>
number: {{ number }}
</div>
</template>
<script setup lang="ts">
import {ref} from "vue";
import C3 from "@/components/c3.vue";
const isShow = ref(true)
</script>
<template>
<div>
<div>
<button @click="isShow=true">显示</button>
<button @click="isShow=false">销毁</button>
</div>
<div>
<c3 v-if="isShow"></c3>
</div>
</div>
</template>
子组件number++之后,父组件销毁子组件,再显示组件,number会重新归0
应该很好理解,因为组件销毁了,组件再重新创建的时候,number重新赋值了
使用keep-alive的效果就是,响应式状态会被保留
用法也很简单,直接使用keep-alive包裹对应组件就好
<keep-alive>
<c3 v-if="isShow"></c3>
</keep-alive>
使用keep-alive之后,在组件内部就会触发onActivated, onDeactivated生命周期函数
<script setup lang="ts">
import {ref} from "vue";
import {onActivated, onDeactivated} from "vue";
const number = ref(0)
onActivated(() => {
console.log("组件激活")
})
onDeactivated(() => {
console.log("组件隐藏")
})
</script>
<template>
<div>
我是c3组件
<br>
<button @click="number++">number++</button>
<br>
number: {{ number }}
</div>
</template>
通过属性控制缓存范围
keep-alive 的 include 和 exclude 就是用来划定缓存范围的 “筛选器”,它们的规则很清晰:
| 属性名 | 作用 | 支持的格式 | 匹配依据 |
|---|---|---|---|
include |
只有名称匹配的组件会被缓存 | 字符串(逗号分隔)、正则、数组 | 组件的 name 选项 |
exclude |
名称匹配的组件不会被缓存 | 字符串(逗号分隔)、正则、数组 | 组件的 name 选项 |
关键前提:要匹配的组件必须显式声明 name 选项(比如 export default { name: 'ListPage' }),否则 include/exclude 无法识别。
我只想让c3缓存
在c3那个组件里面写上组件名
defineOptions({
name: "c3"
})
使用keep-alive的时候指定include
<keep-alive include="c3">
<c3 v-if="isShow"></c3>
</keep-alive>
这个配置的玩法可以配置在路由里面,可以动态指定哪些路由可以被缓存,哪些路由不被缓存
transition动画组件
作用是给单个组件 / 元素的插入、更新、移除过程添加过渡动画,让页面交互更流畅自然。
transition 是 Vue 专门处理 “元素 / 组件进入 / 离开 DOM” 动画的组件,它本身不会渲染成 DOM 节点,而是通过自动检测包裹内容的状态变化(如 v-if/v-show、组件切换),为动画过程添加 / 移除预设的 CSS 类名,从而触发过渡效果。
核心动画阶段(以进入动画为例)
- 进入前:元素还未插入 DOM,添加初始样式类;
- 进入中:元素插入 DOM,添加动画过程类;
- 进入后:动画完成,移除过程类,保留结束样式类(可选);离开动画则是反向流程。
<template>
<div>
<!-- 包裹需要动画的元素/组件 -->
<transition name="fade">
<div v-if="show" class="box">我是带动画的元素</div>
</transition>
<button @click="show = !show">显示/隐藏</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
const show = ref(false)
</script>
<style scoped>
/* 基础样式 */
.box {
width: 200px;
height: 100px;
background: #42b983;
color: white;
text-align: center;
line-height: 100px;
}
/* 1. 进入/离开的初始状态 */
.fade-enter-from, .fade-leave-to {
opacity: 0; /* 透明 */
transform: translateX(20px); /* 右移20px */
}
/* 2. 进入/离开的动画过程 */
.fade-enter-active, .fade-leave-active {
transition: all 0.5s ease; /* 动画时长0.5s,缓动 */
}
/* 3. 进入完成的状态(可选,默认继承原样式) */
.fade-enter-to, .fade-leave-from {
opacity: 1;
transform: translateX(0);
}
</style>
transition 的 name 属性是类名前缀,默认前缀是 v-(如 v-enter-from),自定义 name="fade" 后,类名变为:
| 类名 | 作用 |
|---|---|
fade-enter-from |
进入动画的起始状态(进入前) |
fade``-enter-active |
进入动画的过程状态(进入中) |
fade``-enter-to |
进入动画的结束状态(进入后) |
fade``-leave-from |
离开动画的起始状态(离开前) |
fade``-leave-active |
离开动画的过程状态(离开中) |
fade``-leave-to |
离开动画的结束状态(离开后) |
常用属性
| 属性名 | 作用 |
|---|---|
name |
自定义过渡类名前缀(避免多个动画类名冲突) |
duration |
手动指定动画时长(单位 ms),如 :duration="500" 或 :duration="{ enter: 300, leave: 500 }" |
appear |
初始渲染时也触发进入动画(默认 false),如 appear |
mode |
组件切换时的动画模式(解决两个组件同时动画的问题):- in-out:先进入动画完成,再执行离开动画- out-in:先离开动画完成,再执行进入动画(最常用) |
JS 钩子动画(复杂动画场景)
如果 CSS 动画满足不了需求(如复杂的运动轨迹、结合第三方动画库),可以用 transition 提供的 JS 钩子函数控制动画:
<template>
<button @click="show = !show">显示/隐藏</button>
<transition
@before-enter="beforeEnter"
@enter="enter"
@after-enter="afterEnter"
@before-leave="beforeLeave"
@leave="leave"
@after-leave="afterLeave"
:css="false"
>
<div v-if="show" ref="boxRef" class="box">JS 控制的动画</div>
</transition>
</template>
<script setup>
import { ref } from 'vue'
const show = ref(false)
const boxRef = ref(null)
// 进入前
const beforeEnter = (el) => {
console.log('beforeEnter')
}
// 进入中(done必须调用,告诉Vue动画完成)
const enter = (el, done) => {
console.log('enter')
done()
}
// 进入后
const afterEnter = (el) => {
console.log('afterEnter 进入动画完成')
}
// 离开相关钩子(逻辑类似)
const beforeLeave = (el) => {
console.log('beforeLeave')
}
const leave = (el, done) => {
console.log('leave')
done()
}
const afterLeave = (el) => {
console.log('afterLeave')
}
</script>
teleport瞬移组件
日常开发中,有些元素(如弹窗、模态框、通知提示)从逻辑上属于某个组件,但从 UI 层级上,放在原组件 DOM 结构里会被样式(如 overflow: hidden、z-index)限制,导致遮罩层被遮挡、定位异常等问题。Teleport 解决的就是这个 “逻辑归属” 和 “DOM 位置” 分离的问题:
- ✅ 组件逻辑仍属于父组件(能直接使用父组件的 props、方法、响应式数据);
- ✅ DOM 节点被移动到目标位置(如
body下),避免样式层级冲突。
<template>
<div class="parent-component">
<button @click="showModal = true">打开弹窗</button>
<!-- Teleport 核心用法:to 指定目标 DOM 节点 -->
<Teleport to="body">
<!-- 被瞬移的内容:弹窗(逻辑归当前组件,DOM 移到 body 下) -->
<div class="modal" v-if="showModal">
<div class="modal-content">
<h3>我是弹窗</h3>
<p>我的 DOM 在 body 下,但能访问当前组件的 showModal</p>
<button @click="showModal = false">关闭</button>
</div>
</div>
</Teleport>
</div>
</template>
<script setup>
import { ref } from 'vue'
const showModal = ref(false)
</script>
<style scoped>
.parent-component {
margin: 20px;
padding: 20px;
border: 1px solid #eee;
}
.modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.modal-content {
background: white;
color: #222222;
padding: 20px;
border-radius: 8px;
}
</style>
自定义指令
Vue 3 中自定义指令分为全局指令(全项目可用)和局部指令(仅当前组件可用),指令的核心是通过钩子函数响应元素的生命周期(挂载、更新、卸载等),从而实现对 DOM 的操作。
全局指令
以 “防抖点击” 指令 v-debounce 为例,封装通用的点击防抖逻辑:
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
// 定义全局自定义指令 v-debounce
app.directive('debounce', {
// 元素挂载后执行(核心钩子)
mounted(el, binding) {
// 1. 获取指令参数:防抖时间(默认1000ms)、点击回调
const delay = binding.arg || 1000 // arg 是指令参数(如 v-debounce:500)
el.callback = binding.value // value 是指令绑定的值(如 v-debounce="handleClick")
// 2. 封装防抖函数
el.addEventListener('click', () => {
if (el.timer) clearTimeout(el.timer)
el.timer = setTimeout(() => {
el.callback() // 执行点击回调
}, delay)
})
},
// 元素卸载前清理定时器(避免内存泄漏)
beforeUnmount(el) {
clearTimeout(el.timer)
el.removeEventListener('click', el.callback)
}
})
app.mount('#app')
任意组件使用
<template>
<!-- 使用 v-debounce,参数 500 是防抖时间,值是点击回调 -->
<button v-debounce:500="handleClick">防抖点击(500ms)</button>
</template>
<script setup>
const handleClick = () => {
console.log('点击触发(已防抖)')
}
</script>
图片懒加载示例
app.directive("lazy", {
mounted(el, binding){
// 1. 初始占位图
el.src = 'https://xxx.com/loading.png'
// 2. 创建观察者,监听元素是否进入视口
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
// 3. 进入视口,加载真实图片
el.src = binding.value
// 4. 停止监听
observer.unobserve(el)
}
})
// 5. 监听当前元素
observer.observe(el)
},
beforeUnmount(el) {
// 清理观察者,避免内存泄漏
IntersectionObserver.prototype.unobserve.call(null, el)
}
})
局部指令
如果是局部定义就用directives
<template>
<!-- 使用局部指令 v-focus:输入框挂载后自动聚焦 -->
<input v-focus type="text" />
</template>
<script setup>
// 定义局部自定义指令 v-focus
defineOptions({
directives: {
focus: {
mounted(el) {
el.focus()
}
}
}
})
</script>
指令的生命周期
这个自定义指令也和组件一样,有7个生命周期
| 钩子函数 | 触发时机 | 常用操作 |
|---|---|---|
created |
元素属性 / 事件监听器应用前(元素未挂载) | 初始化数据,不操作 DOM |
beforeMount |
指令绑定到元素后,元素挂载到 DOM 前 | 预操作(如设置初始样式) |
mounted |
元素挂载到 DOM 后(最常用) | 操作 DOM(如绑定事件、初始化) |
beforeUpdate |
组件更新前(元素未重新渲染) | 准备更新 DOM |
updated |
组件更新后(元素已重新渲染) | 同步更新 DOM 状态 |
beforeUnmount |
元素从 DOM 卸载前 | 清理操作(如移除事件监听器) |
unmounted |
元素从 DOM 卸载后 | 最终清理(如销毁定时器) |
每个钩子函数接收 4 个参数:
el:指令绑定的 DOM 元素(可直接操作);binding:指令绑定信息(value/arg/modifiers等);vnode:Vue 编译后的虚拟节点;prevVnode:上一个虚拟节点(仅beforeUpdate/updated可用)。
需要知道哪些情况下会触发对应的钩子函数
app.directive("zl", {
created(el, binding, vnode, prevVNode) {
console.log("created")
},
beforeMount(el, binding, vnode, prevVNode) {
console.log("beforeMount")
},
mounted(el, binding, vnode, prevVNode) {
console.log("mounted", el, binding)
},
beforeUpdate(el, binding, vnode, prevVNode) {
console.log("beforeUpdate")
},
updated(el, binding, vnode, prevVNode) {
console.log("updated")
},
beforeUnmount(el, binding, vnode, prevVNode) {
console.log("beforeUnmount")
},
unmounted(el, binding, vnode, prevVNode) {
console.log("unmounted")
}
})