第三章、Composition Api补充及组件通信

一、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 中用 $nextTickmounted 直接执行
执行时机组件创建后(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✅ 存在,单独存放作用域插槽❌ 合并至 $slotsVue 3 中所有插槽均为函数形式,统一通过 $slots 访问
$listeners✅ 存在,单独存放未被消费的事件监听器❌ 合并至 $attrsVue 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的所有属性(不包括 classstyle)。通过v-bind="$attrs",我们可以将这些属性传递给子组件中的某个具体元素,而无需手动声明每个属性。

使用场景示例:
  1. 封装第三方组件‌:比如在封装 Element Plus 的 el-dialog 时,可以通过 $attrs 将父组件传入的所有属性(如 widthtitle 等)透传给 el-dialog,而不需要逐一声明。

    <template>
      <el-dialog v-bind="$attrs">
        <slot></slot>
      </el-dialog>
    </template>
     
    
  2. 避免冗余props声明‌:当一个组件需要接收大量来自父组件的属性,但又不想一一声明为 props 时,使用 $attrs 可以简化逻辑。

  3. 多根节点组件透传‌:在 Vue 3 中,如果组件有多个根节点,需要显式绑定 $attrs,否则会抛出警告。通过 v-bind="$attrs" 可以将属性绑定到合适的子元素上。

注意事项:

  • $attrs 不包含 classstyle,它们会被自动应用到组件的根元素上。

  • 如果不希望组件根元素继承属性,可以设置 inheritAttrs: false,然后手动绑定 $attrs 到目标元素。

6.5、面试题:$listeners 的妙用

$listeners 在 Vue 2 中用于存储父组件绑定到子组件但未被声明为 emits 的事件监听器。它可以通过 v-on="$listeners" 向下传递给子组件,从而实现事件的透传。

使用场景示例:
  1. 事件透传‌:在封装组件时,若子组件需要将事件传递给更深层的子组件(如孙组件),可以使用 $listeners 实现。

    <template>
      <GrandChildComponent v-on="$listeners" />
    </template>
     
    
  2. 提升组件复用性‌:通过 $listeners,子组件可以将父组件绑定的事件直接传递给内部的子组件,避免中间组件成为“事件中转站”。

Vue 3 中的变化:

在 Vue 3 中,$listeners 已被移除,其功能被整合进了 $attrs 中。所有未被声明为 emits 的事件都将以 onXXX 的形式出现在 $attrs 中。

6.6、面试题:结合使用提升组件灵活性

在实际开发中,$attrs$listeners 经常一起使用,尤其是在封装高级组件时。例如,一个表单组件可能需要接收所有来自父组件的属性和事件,并将其传递给内部的输入控件。

<template>
  <input v-bind="$attrs" v-on="$listeners" />
</template>
 

这种方式不仅简化了代码,还增强了组件的可扩展性。

总结
  1. $attrs 用于传递非props的属性,尤其适用于组件封装和属性透传。

  2. $listeners 在Vue 2中用于传递事件监听器,Vue 3中已整合进 $attrs

  3. 两者结合使用,可以在不增加复杂度的情况下,实现灵活的组件通信。

通过合理使用$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>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值