Attributes 透传

透传 Attributes 指的是组件中未被显式声明(定义)为 props 的属性(如 idclassstyle、自定义属性等)会自动传递到组件的根元素或通过手动设置传递到指定的 DOM 元素的一种行为

这功能依赖于 Vue 的 $attrs 对象,它存储了所有未被组件声明为 props 的属性

默认透传

如果父组件向子组件传递了某些属性,而子组件没有在 props 中声明这些属性,那么这些属性会自动添加到子组件的根元素上

例:

1
2
3
4
5
6
7
8
<!-- src/components/Parent.vue -->
<template>
<Child id="main" class="highlight" custom-attr="test" />
</template>

<script setup>
import Child from './Child.vue'
</script>
1
2
3
4
<!-- src/components/Child.vue -->
<template>
<div>Child Content</div>
</template>
1
2
<!-- DOM渲染结果 -->
<div id="main" class="highlight" custom-attr="test">Child Content</div>

手动透传

如果组件有多个根节点,或者需要将透传的属性应用到某个特定的元素上,可以使用 $attrs

例:

1
2
3
4
5
6
7
8
<!-- src/components/Parent.vue -->
<template>
<Child id="main" class="highlight" custom-attr="test" />
</template>

<script setup>
import Child from './Child.vue'
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- src/components/Child.vue -->
<template>
<div>
<h1>Title</h1>
<div v-bind="$attrs">Child Content</div> <!-- 将属性绑定到指定元素 -->
</div>
</template>

<script setup>
import { useAttrs } from 'vue';

const attrs = useAttrs(); // 获取 $attrs 对象
console.log(attrs); // { id: "main", class: "highlight", custom-attr: "test" }
</script>
1
2
3
4
5
6
7
<!-- DOM渲染结果 -->
<div>
<h1>Title</h1>
<div id="main" class="highlight" custom-attr="test">
Child Content
</div>
</div>

阻止默认透传

如果不希望未声明的属性透传到组件的根元素,可以通过 inheritAttrs: false 禁止默认透传

例:

1
2
3
4
5
6
7
8
9
10
<!-- src/components/Child.vue -->
<template>
<div>Child Content</div>
</template>

<script>
export default {
inheritAttrs: false, // 禁止默认透传, Vue 不会自动将 $attrs 应用到根元素上, 但 $attrs 本身仍然可以手动使用
};
</script>
1
2
3
4
5
6
7
8
<!-- src/components/Parent.vue -->
<template>
<Child id="main" class="highlight" />
</template>

<script setup>
import Child from './Child.vue'
</script>
1
2
3
<!-- DOM渲染结果 -->
<div>Child Content</div>
<!-- 尽管父组件传递了 id 和 class,但它们不会出现在子组件中 -->

用途:

  • HTML属性透传: 动态传递 HTML 元素属性
  • 事件透传: 配合 v-on="$attrs",可以将所有未声明的事件绑定到子组件的元素上
  • 透传到指定元素上: 如果组件需要将父组件的传递属性分发到非根节点,可以结合 $attrsv-bind 使用

透传机制增强了组件的灵活性,避免了硬编码特定的 props,同时也提供了精细化的透传控制

Slots 插槽

Vue 插槽 (Slot) 是用来让父组件向子组件中传递结构化的 DOM 内容。它的作用就像“占位符”,父组件可以把内容填充到子组件的指定位置

默认插槽

例:

子组件中定义插槽位置:

1
2
3
4
5
6
7
<!-- Child.vue -->
<template>
<div class="child">
<h3>子组件</h3>
<slot>我是默认内容</slot> <!-- 占位符 -->
</div>
</template>

父组件使用:

1
2
3
4
5
<template>
<Child>
<p>我是父组件传递过来的内容</p>
</Child>
</template>

渲染 DOM 结果:

1
2
3
4
<div class="child">
<h3>子组件</h3>
<p>我是父组件传递过来的内容</p>
</div>

具名插槽

通过 <slot> 上的 name 属性区分插槽,通过 v-slot:<插槽名> 将 html 插入到指定插槽

例:

子组件:

1
2
3
4
5
6
7
8
9
10
11
12
<!-- Child.vue -->
<template>
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot> <!-- 默认插槽 -->
</main>
<footer>
<slot name="footer"></slot>
</footer>
</template>

父组件:

1
2
3
4
5
6
7
8
9
10
11
<Child>
<template v-slot:header> <!-- 简写: <template #header> -->
<h1>这里是头部</h1>
</template>

<p>这里是主要内容</p>

<template v-slot:footer> <!-- 简写: <template #footer> -->
<p>这里是底部</p>
</template>
</Child>

作用域插槽

插槽不仅能传结构,还能传数据。子组件可以向插槽暴露一些数据,父组件可以接收并渲染

例:

子组件:

1
2
3
4
5
6
7
8
9
10
<!-- Child.vue -->
<template>
<div>
<slot :msg="msg"></slot>
</div>
</template>

<script setup>
const msg = "这是子组件传递的数据"
</script>

父组件:

1
2
3
<Child v-slot="{ msg }">
<p>父组件接收到:{{ msg }}</p>
</Child>

渲染 DOM 结果:

1
2
3
<div>
<p>父组件接收到:这是子组件传递的数据</p>
</div>

Lifecycle 组件生命周期

组件生命周期是指组件从创建、挂载、更新到卸载的一系列过程。每个阶段都有特定的生命周期钩子函数( Hooks Function )供开发者执行相关逻辑

简单的说生命周期函数就是会在某一时刻由 Vue 自动执行的函数

组件生命周期分为以下几个阶段:

阶段钩子函数( 选项式API | 组合式API )描述
创建阶段beforeCreate | 无组件实例创建之前执行,此时无法访问到 propsdata
created | setup组件实例创建完成后执行,此时可以访问到 propsdata
挂载阶段beforeMount | onBeforeMount组件挂载到 DOM 之前执行,此时组件实例已经生成了虚拟DOM,但还没有被渲染到实际的DOM元素上
mounted | onMounted组件挂载到 DOM 之后执行,此时组件实例已经被渲染到了实际的DOM元素上
更新阶段beforeUpdate | onBeforeUpdate组件更新之前执行,在此阶段,组件的数据(响应式数据)已经发生了变化,但是DOM节点还没有被重新渲染(未更新视图)
updated | onUpdated组件更新完成后执行,在此阶段,组件的数据(响应式数据)发生了变化,也已经重新渲染了DOM节点(已更新视图)。
卸载阶段beforeUnmount | onBeforeUnmount组件卸载之前执行,在此阶段,组件即将被销毁
unmounted | onUnmounted组件卸载完成后执行,在此阶段,组件已经被销毁,无法访问到组件实例和DOM元素

生命周期钩子使用场景

例1 数据初始化:

1
2
3
4
5
6
7
8
9
10
11
<!-- 在 created 或 onMounted 钩子中执行数据初始化,例如发送网络请求 -->
<script setup>
import { onMounted, ref } from 'vue';

const data = ref([]);

onMounted(async () => {
const response = await fetch('https://api.example.com/data');
data.value = await response.json();
});
</script>

例2 操作DOM:

1
2
3
4
5
6
7
8
9
<!-- 如果需要操作真实DOM,我们可以在 mounted 或 onMounted 狗子中进行 -->
<script setup>
import { onMounted } from 'vue';

onMounted(() => {
const button = document.querySelector('button')
button.addEventListener('click', () => alert('Clicked!'))
})
</script>

例3 事件监听与清理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!-- 在 mounted 或 onMounted 中添加事件监听器,在 unmounted 或 onUnmounted 中清理 -->
<script setup>
import { onMounted, onUnmounted } from 'vue';

onMounted(() => {
window.addEventListener('resize', onResize);
});

onUnmounted(() => {
window.removeEventListener('resize', onResize);
});

const onResize = () => {
console.log('窗口尺寸发生变化');
};
</script>

例4 清理定时器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- 在 beforeUnmount 或 onUnmounted 中清理定时器 -->
<script setup>
import { onMounted, onUnmounted, ref } from 'vue';

const count = ref(0);
let timer;

onMounted(() => {
timer = setInterval(() => count.value++, 1000);
});

onUnmounted(() => {
clearInterval(timer);
});
</script>

开发中应根据场景选择合适的钩子,并在适当阶段清理资源,避免内存泄漏

Composables 组合式函数

组合式函数是一个利用 Vue 的组合式 API 来封装和复用有状态逻辑的函数,通常也被称为 Vue Hooks 函数。组合式函数允许将组件内的逻辑拆分并以独立的单元进行复用和组合

例:

封装鼠标位置监听:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// useMousePosition.js
import { ref, onMounted, onUnmounted } from 'vue';

export function useMousePosition() {
const x = ref(0);
const y = ref(0);

const updateMouse = (event) => {
x.value = event.clientX;
y.value = event.clientY;
};

onMounted(() => {
window.addEventListener('mousemove', updateMouse);
});

onUnmounted(() => {
window.removeEventListener('mousemove', updateMouse);
});

return { x, y };
}

在组件中使用:

1
2
3
4
5
6
7
8
9
<script setup>
import { useMousePosition } from './useMousePosition.js';

const { x, y } = useMousePosition();
</script>

<template>
<div>鼠标位置:X={{ x }}, Y={{ y }}</div>
</template>

组合式函数最佳实践

  1. 优先封装逻辑到组合式函数

    • 如果某段逻辑可能被多处使用(如窗口监听、数据请求、定时器管理),优先封装为组合式函数
    • 保持组件内的代码精简,集中于组件本身的职责
  2. 清晰命名

    • 组合式函数通常以 useXxx 的命名规范命名
    • 组合式函数的名称应体现其用途,如 useMousePosition
  3. 避免滥用

    • 组合式函数应用于逻辑复用或复杂功能,不适合将简单逻辑(如计算属性)过度抽象

自定义指令(Custom Directive

Vue 提供了自定义指令的功能,用于给 HTML 元素添加额外的行为。自定义指令可以在模板中直接使用,并在指令的生命周期中执行相应的操作

指令的生命周期函数:

  • beforeMount: 在指令绑定的元素挂载 DOM 之前被调用
  • mounted: 在指令绑定的元素被插入到 DOM 中后被调用
  • beforeUpdate: 在指令所在的组件更新之前被调用
  • updated: 在指令所在的组件更新之后被调用
  • beforeUnmount: 在指令所在的组件卸载之前被调用
  • unmounted: 在指令所在的组件卸载之后被调用

每个钩子函数都包含以下参数:

  • el: 指令所绑定的元素
  • binding: 值为一个对象,用于获取指令绑定的相关信息,其中包括:
    • binding.value: 指令绑定的值,该值就是指令等于号( = )后面的值,语法:v-xxx="值"
    • binding.oldValue: 指令上一次绑定的值( = )
    • binding.arg: 指令绑定的参数, 参数就是指令冒号( : )后面的内容,语法:v-xxx:参数="值"
    • binding.modifiers: 指令绑定的修饰符,修饰符是以点开头的特殊后缀,用于给指令添加额外功能和修改行为,语法:v-xxx.修饰符="值" or v-xxx:参数.修饰符="值"
    • binding.instance: 值为指令绑定时所在组件的组件实例

例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* src/main.js */
import { createApp } from 'vue'

const app = createApp()

// 定义全局 focus 指令
app.directive('focus', {
mounted(el) {
// 将焦点设置到 el 元素上
el.focus()
}
})

/*
自定义指令的命名
kebab-case命名 - 如:global-focus
CamelCase命名 - 如:globalFocus
*/
app.mount('#app')

在模板中使用 focus 指令:

1
2
3
4
<template>
<!-- input 元素自动获取焦点 -->
<input type="text" v-focus>
</template>

插件(Plugins

插件是用来增强 Vue 应用功能的一种机制,它不是单纯的一个组件或函数,而是一种批量注册功能,全局注入能力的工具

插件的作用

插件通常用来为 Vue 添加全局功能,如:

  • 全局注册组件(不需要在每个文件里 import)
  • 全局指令(如 v-focus、v-permission)
  • 全局方法 / 属性(如 $message$confirm

Vue 的官方库或部分第三方库都是基于 Vue 插件机制封装的,如:

  • 官方:Vue Router、Vuex
  • 第三方:国际化 i18n、Element Plus、Axios

插件的结构

一个 Vue 插件只要暴露一个 install 方法就行,Vue 在 app.use() 时会调用它

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// myPlugin.js
export default {
install(app, options) {
// 1. 注册全局组件
app.component('MyButton', {
template: `<button><slot /></button>`
});

// 2. 注册全局指令
app.directive('focus', {
mounted(el) {
el.focus();
}
});

// 3. 添加全局方法
app.config.globalProperties.$sayHello = (name) => {
console.log(`Hello, ${name}!`);
};
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// main.js
import { createApp } from 'vue';
import App from './App.vue';
import myPlugin from './myPlugin.js';

const app = createApp(App);

// 使用插件(会自动调用 install)
app.use(myPlugin, { /* 可传配置参数 */ });

app.mount('#app');
/*
这样:
<MyButton> 组件全局可用
所有模板都能用 v-focus
所有实例里都能用 this.$sayHello('Jack')
*/

ref 模板引用

ref 是模板上的一个特殊 attribute,用于从模板中访问对应的 DOM 元素或子组件实例

DOM 元素引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<input ref="myInput" />
</template>

<script setup>
import { useTemplateRef, onMounted } from 'vue'

// 获取模板中 ref=myInput 引用的 DOM 元素
const myInput = useTemplateRef('myInput')

onMounted(() => {
// 必须挂载之后才能访问
myInput.value?.focus()
})
</script>

nextTick 响应式数据更新到 DOM 后的等待函数:

1
2
3
4
5
6
7
8
import { ref, nextTick } from 'vue'

const show = ref(false)
const openAndFocus = async () => {
show.value = true // 更新数据
await nextTick() // 等待 DOM 更新完, 再向下执行
myInput.value?.focus()
}

子组件实例引用(配合 defineExpose 宏)

在子组件使用 <script setup> 时,默认是私有的;父组件想通过 ref 调用其方法,必须由子组件用 defineExpose() 显式暴露。并且要在任何 await 语法之前调用 defineExpose(),否则会报错

例:

1
2
3
4
5
6
7
8
9
10
11
12
<!-- 子组件 Child.vue -->
<template>
<input ref="myInput" />
</template>

<script setup>
import { ref, useTemplateRef } from 'vue'
const myInput = useTemplateRef('useTemplateRef')
const focus = () => myInput.value?.focus()

defineExpose({ focus }) // 暴露给父组件(必须在 await 语法前调用)
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- 父组件 Parent.vue -->
<template>
<Child ref="childRef" />
<button @click="focusChild">聚焦子组件</button>
</template>

<script setup>
import { useTemplateRef } from 'vue'
import Child from './Child.vue'

const childRef = useTemplateRef('childRef')
const focusChild = () => childRef.value?.focus()
</script>

Tips:

  • 只能在挂载后访问模板引用,在挂载之前访问结果会是 null/undefined
  • v-if 切换为 false 会把元素/子组件卸载,对应引用会回到 null,可以使用 ES6 可选链 ( .? ) 防止空值报错

动态组件(<component>

缓存组件(KeepAlive

过度与动画

当一个元素进入/离开页面,或者某个状态发生变化时,我们希望它平滑变化,而不是瞬间切换,Vue 为了让这种效果更简单,内置了 <transition><transition-group> 组件,它可以实现元素 进入/离开时的过渡/动画列表的过渡/动画

  • <Transition> 组件:包裹需要动画效果的单个元素
  •  <transition-group>:包裹需要动画效果的多个元素,每个元素都需要配置 Key 属性

Vue 在元素 插入、移除 时,会自动给它加上不同阶段的 class ( 类名 ),你只需要定义对应的 CSS 即可

不同时刻样式类名:

  • 控制元素进入的样式

    1. v-enter-from 进入的起点
    2. v-enter-active 进入过程中
    3. v-enter-to 进入的终点
  • 控制元素离开的样式

    1. v-leave-from 离开的起点
    2. v-leave-active 离开过程中
    3. v-leave-to 离开的终点

例1 transition 过度:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<transition name="fade">
<h1 v-if="show">你好,过渡!</h1>
</transition>

<style>
/* 定义 transition 过度 */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease;
}

/* 元素进入到离开是的样式 */
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>

例2 animation 动画:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<transition name="bounce">
<p v-if="show">你好,动画!</p>
</transition>

<style>
/* 定义元素进入过程动画 */
.bounce-enter-active {
animation: bounce-in 0.8s;
}
/* 定义元素离开过程动画 */
.bounce-leave-active {
animation: bounce-in 0.8s reverse;
}
/* 定义关键帧 */
@keyframes bounce-in {
0% { transform: scale(0.5); }
50% { transform: scale(1.2); }
100% { transform: scale(1); }
}
</style>

过度/动画的区别与联系

  • 过渡:适合简单的“从 A 到 B”变化(淡入、移动、缩放等),依赖 CSS 的 transition
  • 动画:适合复杂的连续动作,可以自定义关键帧(CSS @keyframes),依赖 CSS 的 animation / @keyframes
  • Vue Transition 组件同时支持过渡和动画,只要你写对应的 CSS 即可