vue3组件化开发
组件化开发是学习这些前端框架最核心的知识,在开发页面的时候,可以先按照页面结构进行组件化拆分 例如: Props Props 是组件的输入,用于接收父组件传递的数据,是父子组件通信最基础的方

vue3组件化开发

发布时间:2025-12-19 (2025-12-19)

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

例如:

Props

Props 是组件的输入,用于接收父组件传递的数据,是父子组件通信最基础的方式。

  1. 核心特性
  • 单向数据流:父组件数据更新 → 子组件 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 大阶段:

  1. 创建阶段:组件实例初始化,尚未挂载到 DOM
  2. 挂载阶段:组件实例挂载到 DOM 树,页面可见
  3. 更新阶段:组件响应式数据变化,触发视图更新
  4. 卸载阶段:组件从 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(refreactivewatchonMounted 等)封装的可复用逻辑函数,命名通常以 use 开头(如 useUseruseLocalStorage

自定义组合式函数(Composables)是 Vue3 基于组合式 API(Composition API) 封装的、可复用的逻辑函数,本质是提取组件中通用的业务逻辑,封装成独立函数,让代码可以跨组件复用,同时让组件代码更简洁、逻辑更清晰。

你可以把它理解为:专门存放 “可复用逻辑” 的 “工具函数”,但和普通工具函数不同的是,它可以直接使用 Vue 的响应式 API(如 refreactivewatch、生命周期钩子等),和组件的上下文深度绑定。

命名规范:通常以 use 开头(如 useCounteruseRequest),这是 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 节点),包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。

  • 适用场景:列表页跳详情页返回、多标签页切换、需要保留表单输入状态的场景等。
  • 核心特性:
    1. 缓存组件实例,避免重复创建 / 销毁
    2. 触发专属生命周期钩子(activated/deactivated
    3. 可通过属性控制缓存范围

比如下面这种情况

<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 类名,从而触发过渡效果。

核心动画阶段(以进入动画为例)

  1. 进入前:元素还未插入 DOM,添加初始样式类;
  2. 进入中:元素插入 DOM,添加动画过程类;
  3. 进入后:动画完成,移除过程类,保留结束样式类(可选);离开动画则是反向流程。
<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: hiddenz-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")
    }
})