一、customRef 自定义Ref
传递一个函数,函数内传入2个参数(都是函数),分别为:track追踪函数、trigger触发视图更新函数
语法使用
customRef((track, trigger) => {
//返回对象
return {
get() {
//追踪的方法 track
},
set() {
//触发更新的方法 trigger
}
}
})
使用代码
<template>
<div>
<input type="text" v-model="msg" />{{ msg }}
</div>
</template>
<script setup>
import { customRef, watch } from 'vue';
//自定义一个防抖函数,返回一个ref对象
function useDebouncedRef(value, delay = 500) {
let timeout;
return customRef((track, triggle) => { //追踪、触发更新
return {
get() {
track();
return value;
},
set(newValue) {
clearTimeout(timeout);
timeout = setTimeout(() => {
value = newValue;
triggle();
}, delay)
}
}
})
}
let msg = useDebouncedRef('');
watch(msg, () => {
console.log('msg被修改了');
})
</script>
二、triggerRef(霸王条款)
强制触发依赖于一个浅层 ref 的副作用。将浅层 ref 的内容强制渲染更新。
示例
<template>
<div>
{{ shallowValue }}
<button @click="handlerClick">更改shallowValue值</button>
</div>
</template>
<script setup>
import { shallowRef, watchEffect, triggerRef } from 'vue';
//声明浅层响应式变量
let shallowValue = shallowRef({
name: "张三"
});
//副作用函数
watchEffect(() => {
//通过value属性查看对象
console.log(shallowValue['value'], "shallowValue被修改了");
});
//处理第一层属性
function handlerClick() {
shallowValue['value'].name = "李四";
triggerRef(shallowValue); //强制更新,重新渲染
};
</script>
三、markRaw
将一个对象标记为不可被转为代理(即:标记当前对象不可响应式),返回该对象本身。
示例
<template>
<div>
{{ reactiveObj.name }}
<button @click="reactiveObj.name = '李四'">更改reactiveObj的值</button>
</div>
</template>
<script setup>
import { markRaw, reactive } from 'vue';
let obj = markRaw({
name: "张三"
});
let reactiveObj = reactive({ obj });
</script>
四、toRaw
返回一个代理对象的目标对象(即:返回原始对象)
示例:Vue3响应式对象与原始对象操作对比
template代码
<template>
<div>
<div>响应式对象proxy的age值:{{ proxy.age }}</div>
<div>原始对象rawObj的age值:{{ rawObj.age }}</div>
<button @click="updateProxy">修改响应式对象的age</button>
<button @click="updateRawObj">修改原始对象的age</button>
</div>
</template>
script代码
<script setup>
import { reactive, toRaw } from 'vue';
//基础原始对象
let target = { age: 18 };
//转为响应式对象
let proxy = reactive(target);
//从响应式代理中获取一个原始对象
let rawObj = toRaw(proxy);
//验证原始对象和toRaw是否是同一个对象
console.log("target === rawObj:", target === rawObj) //true
//修改响应式对象的属性(会触发视图更新)
function updateProxy() {
proxy.age += 1;
};
//修改原始对象的属性(不会触发视图更新)
function updateRawObj() {
rawObj.age += 1;
console.log("原始对象age已修改:", rawObj.age)
}
</script>
核心概念说明
响应式对象 (proxy)
通过 reactive() 创建的代理对象会自动跟踪属性变化,当属性值改变时会触发组件重新渲染。点击第一个按钮时,界面显示会同步更新。
原始对象 (rawObj)
使用 toRaw() 获取的原始对象脱离响应式系统,修改其属性不会触发视图更新(但数据确实会改变,可通过控制台日志验证)。
对象关系验证
target === rawObj 返回 true,说明 toRaw() 返回的是最初传入 reactive() 的原始对象引用。
典型应用场景
- 需要绕过响应式系统进行性能优化时(如大数据量操作)
- 与第三方库交互时需要传递纯JavaScript对象
- 临时操作不希望触发副作用的情况
五、组件通信
5.1、父子通信
父传子(属性传参)
Vue2 实现方式
父组件通过props传递数据给子组件,子组件通过props选项接收。
父组件示例:
<template>
<div>
<children :msg="msg"></children>
</div>
</template>
<script>
import Children from "./V2Children.vue"
export default {
data(){
return {
msg:"来自父组件的消息"
}
},
components: {
Children
}
}
</script>
子组件接收示例:
<template>
<div>
<p>子组件接收的消息:{{ msg }}</p>
</div>
</template>
<script>
export default {
props: {
msg: {
type:String,
default:""
}
}
}
</script>
Vue3 实现方式
父组件通过属性绑定传值,子组件使用 defineProps 宏接收。
父组件示例:
<template>
<div>
<Child :msg="message"></Child>
</div>
</template>
<script setup>
import { ref } from "vue"
import Child from "./V3Children.vue"
let message = ref('父组件传递的信息')
</script>
子组件接收示例:
<template>
<div>
<p>子组件接收的消息:{{ msg }}</p>
</div>
</template>
<script setup>
defineProps({
msg: {
type: String,
default: ""
}
})
</script>
子传父(事件传参)
Vue2 实现方式
子组件通过$emit派发事件,父组件监听事件接收数据。
$emit 派发事件,传入2个参数,分别为:事件名、传参的数据
子组件示例:
<template>
<div class="Children">
<h3>子组件</h3>
<button @click="sendInfoToParent">点击向父组件传递消息</button>
</div>
</template>
<script>
export default {
name: "Children",
data() {
return {
info: {
message: "来自子组件的消息"
}
}
},
methods: {
sendInfoToParent() {
this.$emit("sendInfo", this.info);
}
}
}
父组件示例:
<template>
<div class="Parent">
<Children @sendInfo="receive"></Children>
<p>子组件传递的数据:{{ receiveValue }}</p>
</div>
</template>
<script>
import Children from "./V2Children.vue"
export default {
name: "Parent",
data() {
return {
msg: "父组件传递的消息",
receiveValue: ""
}
},
methods: {
receive(val) {
console.log("子组件传递的数据");
this.receiveValue = val;
}
},
components: {
Children
}
}
Vue3 实现方式
子组件使用defineEmits派发事件,父组件监听事件接收数据。
defineEmit 派发事件,可以传入2个参数,分别为:事件名、传参的数据(可选)
子组件示例:
<template>
<div>
<p>在子组件接收父组件的消息:{{ msg }}</p>
<button @click="sendInfo">发送信息给父组件</button>
</div>
</template>
<script setup>
defineProps({
msg: {
type: String,
default: ""
}
})
let emitFn = defineEmits(["sendInfo"]);
function sendInfo() {
emitFn("sendInfo", "这是子组件传递给父组件的数据")
}
</script>
父组件示例:
<template>
<div>
<Child :msg="message" @sendInfo="receive"></Child>
</div>
</template>
<script setup>
import { ref } from "vue"
import Child from "./V3Children.vue"
let message = ref('父组件传递的信息')
function receive(val){
console.log("子组件传递的数据",val);
}
</script>
5.2、跨组件通信实现
基于父子关系,在对应的父节点进行数据挂载传递给后代节点(组件树共享机制)。
不建议滥用(会造成较大的内存开销,存在数据污染风险)
-
provide
传入2个参数,分别为:事件名/key、传递的值(可以是任意类型,包括响应式数据)(第二个值可选)
-
inject
Vue2 实现方式
使用provide/inject实现跨层级组件通信。
provide示例:
<template>
<div>
<V2Inject></V2Inject>
</div>
</template>
<script>
import V2Inject from './V2Inject.vue';
export default {
data() {
return {
msg: "跨组件传递的数据"
}
},
provide() {
return {
parentInfo: this.msg
}
},
components: {
V2Inject
},
}
</script>
inject示例:
<template>
<div>
<h2>V2Inject接收到provide传来的值</h2>
{{ parentInfo }}
</div>
</template>
<script>
export default {
inject: {
parentInfo: {
default: "默认值"
}
}
}
</script>
Vue3 实现方式
使用provide/inject组合式API实现跨层级通信。
provide示例:
<template>
<div>
<V3Inject />
</div>
</template>
<script setup>
import V3Inject from "./V3Inject.vue";
import { provide } from "vue"
provide("provideInfo", "传递的数据")
</script>
inject示例:
<template>
<div>
<p>接收到的数据:{{ data }}</p>
</div>
</template>
<script setup>
import { inject } from 'vue'
let data = inject("provideInfo", "默认值")
</script>
5.3、eventBus
eventBus(事件总线)基于发布订阅者模式的一个传参机制。
Vue2中的实现方式
Vue2中可直接利用Vue实例实现事件总线机制。需要创建一个专门的EventBus.js文件:
import Vue from "vue";
export const eventBus = new Vue();
推荐将事件总线挂载到Vue原型上(main.js):
import Vue from 'vue'
import App from './App.vue'
import { eventBus } from './EventBus'
Vue.config.productionTip = false
Vue.prototype.$eventBus = eventBus
new Vue({
render: h => h(App),
}).$mount('#app')
发送事件的组件示例:
<template>
<div class='EventBus'>
<button @click="handler">handler传参</button>
<button @click="send">send传参</button>
</div>
</template>
<script>
import { eventBus } from '@/EventBus';
export default {
methods: {
handler() {
eventBus.$emit("sendInfo", this.msg);
},
send() {
this.$eventBus.$emit("message", "传递的数据")
}
}
}
</script>
接收事件的组件示例:
<template>
<div class='EventBusB'>
eventbus接收组件
</div>
</template>
<script>
import { eventBus } from '@/EventBus';
export default {
created() {
eventBus.$on("sendInfo", (value) => {
console.log('接收handler的数据为', value);
});
this.$eventBus.$on("message", (value) => {
console.log('接收send的数据为', value);
});
},
beforeDestroy() {
eventBus.$off("sendInfo");
this.$eventBus.$off("message");
}
}
</script>
Vue3中的实现方案
Vue3需要使用第三方库mitt来实现事件总线。首先安装依赖:
npm i mitt -S
创建事件总线实例(EventBus.js):
import mitt from "mitt";
const eventBus = mitt();
export default eventBus;
发送事件的组件示例:
<template>
<div>
<button @click="send">send传递消息</button>
</div>
</template>
<script setup>
import eventBus from '@/EventBus';
function send() {
eventBus.emit("sendInfo", "eventBus传递的消息");
}
</script>
接收事件的组件示例:
<template>
<div>
接收组件
</div>
</template>
<script setup>
import eventBus from '@/EventBus';
eventBus.on("sendInfo", (value) => {
console.log("接收到eventBus传递的消息", value);
});
</script>
关键差异点
Vue2版本直接使用Vue实例的$emit/$on方法,Vue3版本需要使用mitt库提供的emit/on方法。Vue3中不再需要手动取消订阅,mitt会自动处理组件卸载时的事件清理。
5.4、ref传参
ref是vue的一个属性。
如果ref修饰的是一个vue组件,获取到的是一个组件实例。
如果ref修饰的是一个标签,获取到的是dom元素。
-
$refs 获取所有的ref修饰的内容
-
$nextTick 表示在dom渲染完成后调用(异步函数)
核心区别:created VS mounted+$nextTick
| 维度 | created 中用 $nextTick | mounted 直接执行 |
|---|---|---|
| 执行时机 | 组件创建后(DOM 未渲染)→ 等 DOM 渲染完成后执行 | 组件挂载完成(DOM 已渲染)→ 直接执行 |
| 依赖逻辑 | 必须依赖 $nextTick 等待 DOM 渲染 | 无需额外等待,mounted 本身就是 DOM 渲染完成的钩子 |
| 本质原因 | created 阶段 $refs 还未生成(值为 undefined),必须等 DOM 渲染 | mounted 阶段 $refs 已完全生成,可直接获取 |
vue2
使用ref获取组件实例和DOM元素
PulicCom组件
<template>
<div class='PulicCom'>公共组件</div>
</template>
<script>
export default {
name: 'PulicCom',
data() {
return {
message: '公共的数据'
}
},
methods: {
say() {
console.log('公共的方法被调用了')
}
}
}
</script>
使用
<template>
<div class=''>
<h1 ref="h1">ref传参</h1>
<PublicCom ref="com"></PublicCom>
</div>
</template>
<script>
import PublicCom from './PublicCom.vue';
export default {
name: 'App',
data() {
return {}
},
components: {
PublicCom
},
created() {
this.$nextTick(() => {
let com = this.$refs.com;
let h1 = this.$refs.h1;
console.log(com.message);
com.say();
})
}
}
</script>
vue3
使用 ref 获取组件实例
Child组件
<template>
<div>
子组件
</div>
</template>
<script setup>
import { ref } from 'vue';
let message = ref("hello");
function say() {
console.log("hello");
}
defineExpose({
say,
message
})
</script>
template标签下的公共代码块
<template>
<div>
<Child ref="child"></Child>
</div>
</template>
1、参照vue2的方式
<script setup>
import Child from './Child.vue';
import { getCurrentInstance, nextTick, ref } from 'vue';
let that = getCurrentInstance();
nextTick(() => {
console.log(that.refs.child);
console.log(that.refs.child.message);
that.refs.child.say();
})
</script>
2、使用ref api
-
变量名必须要和对应的ref的值是一致的,否则无法获取。
-
对应的变量名,可以对应获取多个相同 ref 值的对象数组。
<script setup>
import Child from './Child.vue';
import { nextTick, ref } from 'vue';
let child = ref(null);
nextTick(() => {
console.log(child.value.message);
child.value.say();
})
</script>
3、使用useTemplateRef
<script setup>
import Child from './Child.vue';
import { nextTick, useTemplateRef} from 'vue';
let that = getCurrentInstance();
let child = useTemplateRef("child")
nextTick(() => {
console.log(child.value.message);
child.value.say();
})
</script>
相关传参补充
路由传参
状态集中管理(状态变量又被称为响应式变量)
...
六、组件相关内容(面试题:6.3.1、 6.4、6.5、6.6)
Vue实例是Vue应用的根或每个组件对应的实例,它暴露了许多属性和方法。
6.1、Vue实例相关属性
-
$data:组件的响应式数据对象。
-
$props:组件接收的 props 对象。
-
$el:组件挂载的根 DOM 元素。
-
$options:组件初始化时的配置选项。
-
$parent:当前组件的父组件实例。
-
$root:项目根 Vue 实例。
-
$children:当前组件的直接子组件实例数组(Vue2)。
-
$refs:通过 ref 获取的 DOM 元素或子组件实例。
-
$slots:插槽相关,获取所有的插槽内容。
-
$scopedSlots:作用域插槽相关对象。
-
$attrs:包含非prop属性,父组件传递但未被props声明的值,用于跨组件传参。
-
$listeners:监听器对象,父组件绑定但未被消费的事件。
-
$isServer:是否为服务端渲染环境。
-
$ssrContext:服务端渲染上下文。
-
$vnode:组件当前的虚拟节点。
$slots
$slots是Vue中一个非常强大的工具,它不仅允许你访问插槽内容,还可以用于条件渲染、动态插槽名、插槽内容处理等高级用法。通过合理使用$slots,你可以编写出更加灵活和可复用的组件。$attrs和$listeners
在 Vue.js 中,
$attrs和$listeners是两个非常实用的内置属性,尤其在组件封装和跨级通信中具有重要作用。它们能够帮助开发者更优雅地传递属性和事件,减少重复代码,提升组件的复用性和灵活性。
6.2、Vue实例相关方法
-
$watch:观察数据变化。
-
$emit/$on/$once/$off:事件触发与监听(Vue 2中有完整的实例事件,Vue 3中移除了实例的$on、$once、$off,推荐使用外部事件总线或mitt)。
-
$forceUpdate:强制重新渲染。
-
$nextTick:DOM更新后执行回调。
-
$mount/$destroy (Vue 2)/unmount (Vue 3)。
-
$set:为响应式对象添加属性,保证响应式特性。
-
$delete:删除响应式对象属性,触发视图更新。
6.3、Vue 3中的变化:
-
实例上的一些属性被合并或移除,例如$children被移除。
-
实例的事件方法被移除。
-
在组合式API中,可以通过getCurrentInstance()获取内部实例,但通常不需要。
-
通过expose限制暴露给父组件的属性。
6.3.1、面试题:在Vue 3组合式API中如何访问实例属性和方法?
在 <script setup> 中,很少需要直接访问组件实例。但如果有特殊需求(如开发插件、高阶组件),可以使用 getCurrentInstance() 获取内部组件实例:
<script setup>
import { getCurrentInstance } from 'vue';
const instance = getCurrentInstance();
console.log(instance.proxy.$attrs); // 通过proxy访问公共属性
console.log(instance.ctx); // 开发环境下的上下文(包含所有属性)
</script>
6.3.2、Vue 2与Vue 3的实例属性差异
| 属性名 | Vue 2 表现 | Vue 3 表现 | 核心说明 |
|---|---|---|---|
$children | ✅ 存在,返回子组件实例数组 | ❌ 移除 | Vue 3 推荐使用 $refs 或组合式 API 访问子组件 |
$scopedSlots | ✅ 存在,单独存放作用域插槽 | ❌ 合并至 $slots | Vue 3 中所有插槽均为函数形式,统一通过 $slots 访问 |
$listeners | ✅ 存在,单独存放未被消费的事件监听器 | ❌ 合并至 $attrs | Vue 3 中将事件监听器视为属性透传的一部分 |
$attrs | 包含 class/style 及非 props 属性,不包含事件监听器 | ✅ 包含所有透传属性(含事件监听器) | 行为变化,透传属性范围扩大 |
$data | ✅ 响应式数据对象 | ✅ 响应式数据对象 | 基本一致 |
$props | ✅ 接收的 props 对象 | ✅ 接收的 props 对象 | 基本一致 |
$el | ✅ 组件根 DOM 元素 | ✅ 根元素(多根节点时可能为文本节点) | 多根节点场景下行为略有不同 |
$parent / $root | ✅ 父实例 / 根实例 | ✅ 父实例 / 根实例 | 存在,但可能受 <Teleport>、<Suspense> 组件影响 |
$refs | ✅ DOM 元素 / 子组件引用 | ✅ DOM 元素 / 子组件引用 | 基本一致 |
$slots | ✅ 渲染函数中访问插槽内容(普通插槽) | ✅ 包含所有插槽的函数(含作用域插槽) | Vue 3 中插槽均以函数形式存在 |
6.3.3、Vue 2与Vue 3的实例方法差异
| 方法名 | Vue 2 表现 | Vue 3 表现 | 核心说明 |
|---|---|---|---|
$on / $once / $off | ✅ 存在,实例可作为事件发射器监听 / 触发自定义事件 | ❌ 移除 | Vue 3 实例不再是事件发射器,需用 mitt 等外部库实现 |
$watch | ✅ 实例方法,监听数据变化 | ✅ 实例方法(组合式 API 推荐 watch 函数) | 功能保留,推荐写法变化 |
$emit | ✅ 触发自定义事件 | ✅ 触发自定义事件 | 基本一致 |
$forceUpdate | ✅ 强制组件重新渲染 | ✅ 强制组件重新渲染 | 存在,但不推荐滥用 |
$nextTick | ✅ 实例方法,DOM 更新后执行回调 | ✅ 实例方法(也可直接导入 nextTick) | 功能保留,支持单独导入使用 |
6.4、面试题:$attrs 的妙用
$attrs 包含了父组件传递给子组件、但未被声明为props的所有属性(不包括 class 和 style)。通过v-bind="$attrs",我们可以将这些属性传递给子组件中的某个具体元素,而无需手动声明每个属性。
使用场景示例:
-
封装第三方组件:比如在封装 Element Plus 的
el-dialog时,可以通过$attrs将父组件传入的所有属性(如width、title等)透传给el-dialog,而不需要逐一声明。<template> <el-dialog v-bind="$attrs"> <slot></slot> </el-dialog> </template> -
避免冗余props声明:当一个组件需要接收大量来自父组件的属性,但又不想一一声明为
props时,使用$attrs可以简化逻辑。 -
多根节点组件透传:在 Vue 3 中,如果组件有多个根节点,需要显式绑定
$attrs,否则会抛出警告。通过v-bind="$attrs"可以将属性绑定到合适的子元素上。
注意事项:
$attrs不包含class和style,它们会被自动应用到组件的根元素上。如果不希望组件根元素继承属性,可以设置
inheritAttrs: false,然后手动绑定$attrs到目标元素。
6.5、面试题:$listeners 的妙用
$listeners 在 Vue 2 中用于存储父组件绑定到子组件但未被声明为 emits 的事件监听器。它可以通过 v-on="$listeners" 向下传递给子组件,从而实现事件的透传。
使用场景示例:
-
事件透传:在封装组件时,若子组件需要将事件传递给更深层的子组件(如孙组件),可以使用
$listeners实现。<template> <GrandChildComponent v-on="$listeners" /> </template> -
提升组件复用性:通过
$listeners,子组件可以将父组件绑定的事件直接传递给内部的子组件,避免中间组件成为“事件中转站”。
Vue 3 中的变化:
在 Vue 3 中,$listeners 已被移除,其功能被整合进了 $attrs 中。所有未被声明为 emits 的事件都将以 onXXX 的形式出现在 $attrs 中。
6.6、面试题:结合使用提升组件灵活性
在实际开发中,$attrs 和 $listeners 经常一起使用,尤其是在封装高级组件时。例如,一个表单组件可能需要接收所有来自父组件的属性和事件,并将其传递给内部的输入控件。
<template>
<input v-bind="$attrs" v-on="$listeners" />
</template>
这种方式不仅简化了代码,还增强了组件的可扩展性。
总结
$attrs用于传递非props的属性,尤其适用于组件封装和属性透传。
$listeners在Vue 2中用于传递事件监听器,Vue 3中已整合进$attrs。两者结合使用,可以在不增加复杂度的情况下,实现灵活的组件通信。
通过合理使用
$attrs和$listeners,可以显著提高组件的复用性和开发效率。
七、插槽
7.1、概述
在Vue中,插槽(Slots)是组件间通信和内容分发的重要机制。通过插槽,父组件可以向子组件传递内容,使得组件更加灵活和可复用。
7.2、插槽划分
slot-name 指定插槽名,已废弃,name替换
slot-scoped 指定作用域插槽,已废弃,scoped替换
-
匿名插槽(不带名称的
<slot></slot>,默认插槽 default) -
具名插槽(通过 name 属性定义多个插槽,使用时用
v-slot:slotName指定) -
作用域插槽(通过扩展作用的方式,将对应的子组件的数据暴露给父组件,主要是绑定属性的方式传递数据。父组件使用
v-slot="slotProps"接收,实现更灵活的渲染逻辑)
7.3、匿名插槽
定义
<slot></slot>
调用方式
<SlotCom>
<template v-slot:default>
默认插槽内容
</template>
</SlotCom>
7.4、具名插槽
定义
<slot name="content"></slot>
调用方式
<template v-slot:content>
具名插槽内容
</template>
7.5、作用域插槽
定义
<slot name="foot" :user="person" message='数据值'></slot>
调用方式
<template #foot="{ user, message }">
父组件接收数据:{{ user.username }} {{ message }}
<button @click="user.username = '新值'">修改数据</button>
</template>
7.6、Vue插槽使用示例
组件定义部分
<template>
<div class='ComA'>
<slot></slot>
<h1>中间的内容</h1>
<slot name="content"></slot>
<slot name="foot" :user="person" message='数据值'></slot>
<h1>插槽组件的内容:{{ person.username }}</h1>
</div>
</template>
<script>
export default {
name: 'ComA',
data() {
return {
person: {
username: '张三'
}
}
},
mounted() {
console.log(this.$slots)
}
}
</script>
组件使用部分
<template>
<div class='App'>
<SlotCom>
<template v-slot:default>
传递给默认插槽的内容
</template>
<template v-slot:content>传递给content插槽的内容</template>
<template #foot="{ user, message }">
父组件的内容:{{ user.username }} {{ message }}
<button @click="user.username = '更改的值'">更改username</button>
</template>
</SlotCom>
</div>
</template>
<script>
import SlotCom from './SlotCom.vue';
export default {
name: 'App',
data() {
return {}
},
components: {
SlotCom
},
mounted() {
console.log(this.$children)
}
}
</script>
注意事项
- 作用域插槽传递的数据是响应式的
- 新版语法使用
v-slot替代已废弃的slot属性 #是v-slot:的简写语法- 可通过
this.$slots访问插槽内容 - 可通过
this.$children访问子组件实例
八、组件的封装
组件封装三要素
在 Vue 中,组件封装的三要素是:Props(属性)、Slots(插槽) 和 Events(事件)。
Props(属性)
Props 是父组件向子组件传递数据的方式。通过定义 props,子组件可以接收来自父组件的数据,并根据这些数据进行渲染或逻辑处理。
-
Props 用于将数据从父组件传入子组件。
-
子组件应避免直接修改 props,若需修改,可通过事件通知父组件进行更改。
-
合理使用 props 可以提高组件的可复用性和灵活性。
Slots(插槽)
Slots 允许父组件向子组件中插入自定义内容,增强组件的扩展性。
-
插槽(slot)是子组件中预留的位置,供父组件插入内容。
-
可以使用默认插槽、具名插槽和作用域插槽来实现不同的内容分发方式。
-
合理使用插槽可以让组件更通用,适应多种使用场景。
Events(事件)
Events 是子组件向父组件传递信息的方式,通常用于触发父组件中的逻辑或更新状态。
-
子组件通过
$emit触发事件,通知父组件发生了某些行为。 -
父组件监听这些事件并作出响应,实现组件间的通信。
-
通过事件机制,组件之间可以实现松耦合,便于维护和扩展。
这三者构成了 Vue 组件封装的核心结构,使组件具备良好的可复用性、可维护性和扩展性。
示例
模板片段
<template>
<nav class="navbar">
<div class="navbar-brand">
<slot name="brand">
<a href="/" class="navbar-logo">Logo</a>
</slot>
</div>
<div class="navbar-menu" :class="{ 'is-active': mobileMenuActive }">
<div class="navbar-start">
<slot name="menu-items">
<a
v-for="(item, index) in menuItems"
:key="index"
:href="item.href"
class="navbar-item"
:class="{ 'is-active': item.active }"
>
{{ item.text }}
</a>
</slot>
</div>
<div class="navbar-end">
<slot name="actions">
<div class="navbar-item">
<button class="button is-primary">登录</button>
</div>
</slot>
</div>
</div>
<div class="navbar-burger" @click="toggleMobileMenu">
<span></span>
<span></span>
<span></span>
</div>
</nav>
</template>
脚本片段
<script>
export default {
name: 'Navbar',
props: {
menuItems: {
type: Array,
default: () => []
}
},
data() {
return {
mobileMenuActive: false
}
},
methods: {
toggleMobileMenu() {
this.mobileMenuActive = !this.mobileMenuActive
this.$emit('mobile-menu-toggle', this.mobileMenuActive)
}
},
watch: {
mobileMenuActive(newVal) {
if (newVal) {
document.body.classList.add('is-clipped')
} else {
document.body.classList.remove('is-clipped')
}
}
}
}
</script>
样式片段
<style scoped>
.navbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 2rem;
background-color: #ffffff;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
position: relative;
z-index: 100;
}
.navbar-brand {
display: flex;
align-items: center;
}
.navbar-logo {
font-size: 1.5rem;
font-weight: bold;
color: #333333;
text-decoration: none;
}
.navbar-menu {
display: flex;
align-items: center;
flex-grow: 1;
justify-content: space-between;
}
.navbar-start,
.navbar-end {
display: flex;
align-items: center;
}
.navbar-item {
padding: 0.5rem 1rem;
color: #666666;
text-decoration: none;
transition: color 0.3s ease;
}
.navbar-item:hover {
color: #333333;
}
.navbar-item.is-active {
color: #409eff;
font-weight: 500;
}
.button {
padding: 0.5rem 1rem;
border-radius: 4px;
border: none;
cursor: pointer;
transition: background-color 0.3s ease;
}
.button.is-primary {
background-color: #409eff;
color: white;
}
.button.is-primary:hover {
background-color: #337ecc;
}
.navbar-burger {
display: none;
flex-direction: column;
justify-content: space-around;
width: 2rem;
height: 2rem;
background: transparent;
border: none;
cursor: pointer;
padding: 0;
z-index: 101;
}
.navbar-burger span {
width: 100%;
height: 2px;
background-color: #333333;
transition: all 0.3s ease;
}
@media screen and (max-width: 768px) {
.navbar-menu {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100vh;
background-color: white;
flex-direction: column;
justify-content: flex-start;
padding-top: 4rem;
transform: translateX(100%);
transition: transform 0.3s ease;
z-index: 100;
}
.navbar-menu.is-active {
transform: translateX(0);
}
.navbar-start,
.navbar-end {
flex-direction: column;
width: 100%;
}
.navbar-item {
width: 100%;
text-align: center;
padding: 1rem;
border-bottom: 1px solid #f0f0f0;
}
.navbar-burger {
display: flex;
}
.navbar-burger.is-active span:nth-child(1) {
transform: rotate(45deg) translate(5px, 5px);
}
.navbar-burger.is-active span:nth-child(2) {
opacity: 0;
}
.navbar-burger.is-active span:nth-child(3) {
transform: rotate(-45deg) translate(5px, -5px);
}
}
</style>
2338

被折叠的 条评论
为什么被折叠?



