前端面经-VUE3篇(二)--vue3组件知识(一)组件注册、props 与 emits、透传、插槽(Slot)

组件允许我们将 UI 划分为独立的、可重用的部分,并且可以对每个部分进行单独的思考。在实际应用中,组件常常被组织成一个层层嵌套的树状结构:

一、注册

Vue 组件本质上是一个可以复用的 自定义 HTML 元素,为了在其他组件中使用一个组件,你必须先注册它。注册的过程就是:把组件告诉 Vue,这样 Vue 才能在模板中识别并正确渲染它

注册方式分类(Vue 3)

注册方式说明特点
全局注册应用级注册,所有组件都能使用简便、但增加全局污染
局部注册在组件内部通过 components 注册推荐,更清晰、可维护
<script setup>Composition API 的简化写法Vue 3 推荐,自动引入更方便

 1、全局注册

// main.js 或 main.ts
import { createApp } from 'vue'
import App from './App.vue'
import MyComponent from './components/MyComponent.vue'

const app = createApp(App)

// 注册全局组件
app.component('MyComponent', MyComponent)

app.mount('#app')

特点:

  • 一次注册,整个项目中的模板都能使用 <MyComponent />

  • 缺点是全局污染命名空间,不利于维护,不推荐在大型项目中滥用

 2、局部注册

<script setup>
import MyComponent from './MyComponent.vue'
</script>
<template>
  <MyComponent />
</template>

特点:

  • 只在当前组件作用域内生效

  • 更加清晰、模块化,推荐使用

3、自动注册(文件自动引入)

 在使用 Vite 或 Webpack 的 Vue 项目中,可以借助 import.meta.globrequire.context() 实现自动注册所有组件:

示例(自动全局注册 components 文件夹下所有组件):

// main.js
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
const modules = import.meta.glob('./components/*.vue', { eager: true })
for (const path in modules) {
  const component = modules[path].default
  const name = component.name || path.split('/').pop().replace('.vue', '')
  app.component(name, component)
}
app.mount('#app')

 4、动态组件注册

使用 <component :is="componentName"> 可以动态切换渲染的组件:

<template>
  <component :is="currentComponent" />
</template>

<script setup>
import ComponentA from './ComponentA.vue'
import ComponentB from './ComponentB.vue'

const currentComponent = ref('ComponentA')
</script>

 需要配合预先注册这些组件或使用异步加载。

5、 Vue 3 <script setup> 的注册简化优势

从 Vue 3.2 起,推荐使用 <script setup>,其优势是:

  • 无需写 components 选项;

  • 直接引入即可自动生效;

  • 模板中可直接使用变量名作为标签。

<script setup>
import MyComponent from './MyComponent.vue'
</script>

<template>
  <MyComponent />
</template>

这种方式既是局部注册,又是最现代、推荐的写法。

二、组件通信(prop与emit)

Vue 的组件是树状结构,存在父子关系、兄弟关系、跨层级关系:

  • 父 → 子:传递数据(props

  • 子 → 父:触发事件(emit

  • 兄弟之间或跨层通信:需要状态共享或事件中心

掌握组件通信的方式,是 Vue 构建中大型项目的基础。

Vue 组件通信方式总览(Vue 3)

类型通信方式说明
父 → 子props父组件向子组件传递数据
子 → 父emit子组件向父组件发送事件
双向绑定v-model基于 modelValueupdate:modelValue
任意组件间状态管理(如 Pinia)共享状态,兄弟 / 跨层通信
任意组件间provide/inject祖先 → 后代,非响应式(默认)
任意组件间事件总线(不推荐)Vue 3 中已不推荐,Pinia 更优
父/子操作子/父组件实例ref 绑定组件访问子组件方法或属性

 1、父传子通信(props

1、什么是 props

props 是指 父组件通过 HTML 属性的形式,向子组件传递的数据

你可以把 props 理解为组件的“参数”
父组件在使用子组件时,像调用函数一样传入值,而子组件通过 props接收值并使用

2、基本使用

 父组件传值:

<!-- Parent.vue -->
<MyCard title="Vue 面经" content="面试知识点" />

 子组件接收值:

<!-- MyCard.vue -->
<script setup>
const props = defineProps(['title', 'content'])
</script>
<template>
  <div>
    <h3>{{ props.title }}</h3>
    <p>{{ props.content }}</p>
  </div>
</template>

 在 <script setup> 中你也可以这样写更简洁:

const { title, content } = defineProps(['title', 'content'])

 除了使用字符串数组来声明 props 外,还可以使用对象的形式:

// 使用 <script setup>
defineProps({
  title: String,
  content: String})

对于以对象形式声明的每个属性,key 是 prop 的名称,而值则是该 prop 预期类型的构造函数。比如,如果要求一个 prop 的值是 number 类型,则可使用 Number 构造函数作为其声明的值。

对象形式的 props 声明不仅可以一定程度上作为组件的文档,而且如果其他开发者在使用你的组件时传递了错误的类型,也会在浏览器控制台中抛出警告。

Vue 中 props只读的

  • 子组件可以读取 props 的值;

  • 但不可以修改它,因为这可能会破坏父组件的状态管理。 

 如果你确实需要修改,可以复制到一个 ref:const localTitle = ref(props.title)

更改对象 / 数组类型的 props

当对象或数组作为 props 被传入时,虽然子组件无法更改 props 绑定,但仍然可以更改对象或数组内部的值。这是因为 JavaScript 的对象和数组是按引用传递,对 Vue 来说,阻止这种更改需要付出的代价异常昂贵。

这种更改的主要缺陷是它允许了子组件以某种不明显的方式影响父组件的状态,可能会使数据流在将来变得更难以理解。在最佳实践中,你应该尽可能避免这样的更改,除非父子组件在设计上本来就需要紧密耦合。在大多数场景下,子组件应该抛出一个事件(emit)来通知父组件做出改变。

 3、响应式 Props 解构 ​

Vue 的响应式依赖追踪机制是这样的:

  • 当你在一个 计算属性watchEffect / watch 函数中访问某个响应式数据的属性,Vue 会自动收集这个属性作为依赖。

  • 当这个属性变化时,对应的副作用函数就会被重新执行。

 3.4 及以下版本的问题:props 解构会“失去响应性”

const { foo } = defineProps(['foo'])

watchEffect(() => {
  console.log(foo)
})


在 Vue 3.4 及以前版本:
这里 foo 是普通值(不是 ref 或 reactive 的值),不会追踪变更。
所以 watchEffect() 只执行一次,不会在 foo 更新时再次触发。

 Vue 3.5 的改进:解构的 props 自动补充响应式追踪

 在 Vue 3.5 中,Vue 编译器做了优化:

const { foo } = defineProps(['foo'])
等价于:
const props = defineProps(['foo'])

watchEffect(() => {
  console.log(props.foo) // Vue 编译器自动转换为 props.foo
})

这就意味着

  • 解构的变量 foo 仍然是响应式的**(由编译器自动转换)**;

  • watchEffectcomputed 等副作用函数中访问 foo 时可以正常追踪变化

  • 无需再使用 toRefs() 手动保持响应性,前提是你是在 <script setup> 的作用域内直接用。

 注意:在传递到函数时不会保留响应性!

const { foo } = defineProps(['foo'])

watch(foo, () => {
  // ❌ 不会追踪 foo 的变化,因为 foo 是值,不是响应式引用
})

这是一个很容易踩的坑!
在 watch(foo) 这种写法中,foo 被当作一个“值”传入,而不是 getter;
所以watch 只监听了一次值,不会追踪变化,Vue 也会在控制台发出警告。

 正确做法:将解构后的 prop 包装成 getter

watch(() => foo, () => {
  // ✅ 正确,foo 的变化会被追踪
})
为什么这样可以?
因为 () => foo 是一个 getter 函数;
Vue 会自动追踪 getter 内部访问的响应式属性;
所以 foo 的变化会被正确响应。

 在 Vue 3.5 中,解构的 props 在 watchEffect/computed 中是响应式的,但在函数调用/事件中不是,此时要用 () => foo 包装成 getter。

 4、props 类型声明和校验

Vue 3 推荐的方式:

const props = defineProps<{
  title: string
  content?: string
}>()

使用对象形式提供更多配置:

const props = defineProps({
  title: {
    type: String,
    required: true
  },
  content: {
    type: String,
    default: '默认内容'
  }
})
选项作用
type声明类型,如 String、Number 等
required是否必传
default默认值

 Vue 组件可以更细致地声明对传入的 props 的校验要求。如果传入的值不满足类型要求,Vue 会在浏览器控制台中抛出警告来提醒使用者。这在开发给其他开发者使用的组件时非常有用。

要声明对 props 的校验,你可以向 defineProps() 宏提供一个带有 props 校验选项的对象,例如:

defineProps({
  // 基础类型检查
  // (给出 `null` 和 `undefined` 值则会跳过任何类型检查)
  propA: Number,
  // 多种可能的类型
  propB: [String, Number],
  // 必传,且为 String 类型
  propC: {
    type: String,
    required: true
  },
  // 必传但可为 null 的字符串
  propD: {
    type: [String, null],
    required: true
  },
  // Number 类型的默认值
  propE: {
    type: Number,
    default: 100
  },
  // 对象类型的默认值
  propF: {
    type: Object,
    // 对象或数组的默认值
    // 必须从一个工厂函数返回。
    // 该函数接收组件所接收到的原始 prop 作为参数。
    default(rawProps) {
      return { message: 'hello' }
    }
  },
  // 自定义类型校验函数
  // 在 3.4+ 中完整的 props 作为第二个参数传入
  propG: {
    validator(value, props) {
      // The value must match one of these strings
      return ['success', 'warning', 'danger'].includes(value)
    }
  },
  // 函数类型的默认值
  propH: {
    type: Function,
    // 不像对象或数组的默认,这不是一个
    // 工厂函数。这会是一个用来作为默认值的函数
    default() {
      return 'Default function'
    }
  }
})

4、在模板中使用 props:

你可以像使用普通变量一样直接使用 props 中的数据。  <p>{{ content }}</p>

5、动态绑定 props:使用 v-bind

使用 v-bind 或缩写 : 来进行动态绑定的 props:

<!-- 根据一个变量的值动态传入 -->
<MyCard :title="post.title" />

<!-- 根据一个更复杂表达式的值动态传入 -->
<MyCard :title="post.title + ' by ' + post.author.name" />

或者使用 v-bind 对象方式:
<MyButton v-bind="{ label: buttonText, disabled: true }" />

使用一个对象绑定多个 prop

如果你想要将一个对象的所有属性都当作 props 传入,你可以使用
没有参数的v-bind ,即只使用 v-bind 而非 :prop-name。例如,这里有一个 post 对象: 

 const post = { ititle: 'My Journey with Vue' ,content:'面试知识点'}

以及下面的模板:<MyCard v-bind="post" />

而这实际上等价于:<MyCard :title="post.title" :content="post.content" />

6、布尔类型的特殊写法 

Vue 中的布尔型 props 可以使用属性存在即为 true的简写方式:

<!-- 等价于 :isLoading="true" -->
<LoadingSpinner isLoading />

7、CamelCase 与 kebab-case 的映射

Vue 支持驼峰式声明 props:const props = defineProps(['isLoading'])

但是在模板中使用组件时应使用 kebab-case<MyComponent is-loading />

2、子 → 父:使用 emit事件

 在 Vue 中,emit 是子组件向父组件发送事件的核心机制,它是实现子 → 父通信的标准方式。理解和正确使用 emit 对于掌握组件通信和构建响应式组件非常重要,尤其在面试和组件封装时经常考查。

1、什么是 emit

emit 是 Vue 组件中用来在子组件中触发自定义事件,通知父组件做出响应的方法。

  • 父组件可以监听这些事件,就像监听原生 DOM 事件一样。

  • 子组件不会直接操作父组件的数据,而是通过 emit 发出事件,由父组件处理。

 2、最基础的使用方式

 子组件:触发事件

<!-- MyButton.vue -->
<script setup>
const emit = defineEmits(['click'])
</script>
<template>
  <button @click="emit('click')">点击</button>
</template>

🌱 父组件:监听事件

<!-- Parent.vue -->
<MyButton @click="handleClick" />

<script setup>
function handleClick() {
  console.log('子组件点击事件触发了')
}
</script>

3、emit 的参数传递

 你可以在 emit 时传递参数,供父组件使用:

 子组件:emit('select', itemId)

 父组件:<MyItem @select="onItemSelect" /> 

function onItemSelect(id) {
  console.log('你选择了 ID:', id)
}

4、defineEmits() 的类型定义 

1、什么是 defineEmits()

defineEmits()<script setup> 中用来 声明和使用自定义事件 的语法糖(宏函数):

  • 它的作用等价于传统组件的 emits 选项;

  • 并返回一个名为 emit 的函数,供你在组件内触发事件;

  • 只能在 <script setup>顶层作用域中使用。

 2、基本用法:声明 + 触发事件
<script setup>
const emit = defineEmits(['submit', 'inFocus'])

function onClick() {
  emit('submit') // 触发“submit”事件
}
</script>

解释:

  • defineEmits(['submit']) 表示这个组件会触发名为 'submit' 的事件;

  • defineEmits接收一个字符串数组,每个字符串代表一个事件名

  • emit('submit') 就是在组件内部触发这个事件;

  • 父组件就可以通过 <MyForm @submit="handleSubmit" /> 来监听。

 为什么要显式声明事件?

虽然 emit() 可以直接使用事件名,但声明事件有很多好处

好处说明
🧠 文档化让组件的可用事件一目了然
✅ 类型校验在 TypeScript 下自动提示事件名和参数
💡 IDE 补全支持事件名的自动补全
⚠️ 区分 DOM 属性帮助 Vue 更好地区分自定义事件和 HTML 属性(比如 focusinFocus

  在非 <script setup> 中如何使用?

 export default {
  emits: ['submit', 'inFocus'], // 显式声明
  setup(props, ctx) {
    ctx.emit('submit') // 触发事件
  }
}

或者解构方式:

 setup(props, { emit }) {
  emit('submit')
}

 在选项式组件中,事件由 emits 选项声明,通过 ctx.emit() 或解构 emit() 使用。

 对象语法 + 参数校验(Vue 支持)

const emit = defineEmits({
  submit(payload: { email: string; password: string }) {
    return payload.email.includes('@') && payload.password.length > 6
  }
})

说明:

  • 这是对象语法,事件名作为 key;

  • value 是一个验证函数,用于校验参数合法性;

  • 如果返回 false,Vue 会在 dev 环境报出 warning,提醒你传参错误。

 TypeScript 的推荐写法(强类型声明)

const emit = defineEmits<{
  (e: 'change', id: number): void
  (e: 'update', value: string): void
}>()

3、重要规则和限制

限制说明
顶层使用defineEmits() 只能写在 <script setup> 顶部作用域
不可嵌套不能在函数体或条件判断里调用
返回值是 emit()用它触发事件(而不是 $emit()
TypeScript 推荐使用泛型定义事件名和参数

5、事件校验

在 Vue 中我们熟悉了对 props 做类型校验,比如:

const props = defineProps({
  title: String
})

defineEmits() 也支持类似的“事件参数校验”,尤其是在你希望防止父组件传错数据提高组件代码自解释性时很有用。

 示例:

<script setup>
const emit = defineEmits({
  // 没有校验
  click: null,

  // 校验 submit 事件
  submit: ({ email, password }) => {
    if (email && password) {
      return true
    } else {
      console.warn('Invalid submit event payload!')
      return false
    }
  }
})

function submitForm(email, password) {
  emit('submit', { email, password })
}
</script>

你声明了两个事件:

事件名校验方式说明
clicknull没有参数校验,随便触发
submit函数有参数校验逻辑,必须传入合法对象

submit 校验函数解释:

  • 接收的是你 emit('submit', { ... }) 传入的参数;

  • 返回 true 表示校验通过;

  • 返回 false 则该事件不会被触发(Vue 会阻止它),并在控制台输出警告。

什么时候使用事件校验? 

适用场景示例
表单提交事件需要 email + password防止少传字段或格式错误
对自定义事件要求固定格式{ id: number, status: string }
组件复杂、文档难覆盖时用事件校验机制辅助防错
团队协作开发强约束组件输入,提高可维护性

事件校验 vs 类型提示

特性类型提示(TS 泛型)参数校验(对象语法)
校验时间编译时运行时
表现方式IDE 提示、自动补全控制台 warning / 阻止触发
安全性提前发现问题运行时保护
推荐搭配强烈建议一起使用

 推荐组合用法(TS + 校验):

const emit = defineEmits<{
  (e: 'submit', payload: { email: string, password: string }): void
}>({
  submit(payload) {
    return !!payload.email && !!payload.password
  }
})

说明:

  • 让你同时拥有类型安全 + 运行时验证

  • 避免父组件漏传或传错参数;

  • 自动提示 + 编译保护 + 控制台预警 = 组件更安全!

三、组件的v-model

v-model 是 Vue 提供的一个语法糖,用于在 父组件和子组件之间同步数据

 它相当于下面两件事的组合

<!-- 相当于 -->
<MyInput
  :modelValue="username"
  @update:modelValue="username = $event"
/>

换句话说,v-model="username" 背后做了两件事:
父组件将 username 作为 modelValue 传给子组件;
子组件通过 emit('update:modelValue', newValue) 通知父组件更新。

 1、在子组件中实现 v-model

 子组件 MyInput.vue

<script setup>
const props = defineProps(['modelValue'])         // 接收值
const emit = defineEmits(['update:modelValue'])   // 声明事件

function onInput(event) {
  emit('update:modelValue', event.target.value)   // 通知父组件更新
}
</script>

<template>
  <input :value="modelValue" @input="onInput" />
</template>

 父组件使用:<MyInput v-model="username" />
现在 username 就在父子组件之间保持同步了 ✅

2、为什么叫 modelValue

在 Vue 3 中:

  • 默认 v-model 会绑定到 modelValue

  • 触发事件名为 update:modelValue

  • 这是一个约定俗成的机制,是 Vue 内部解析 v-model 的基础。

支持多个 v-model:自定义参数名 

Vue 3 新特性:可以给 v-model 自定义参数名,实现多个绑定! 

<MyComponent
  v-model:title="postTitle"
  v-model:content="postContent"
/>

对应子组件写法:

const props = defineProps(['title', 'content'])
const emit = defineEmits(['update:title', 'update:content'])

function updateTitle(val) {
  emit('update:title', val)
}
function updateContent(val) {
  emit('update:content', val)
}

模板中:

<input :value="title" @input="updateTitle($event.target.value)" />
<textarea :value="content" @input="updateContent($event.target.value)" />

 这样你就可以用多个 v-model 在一个组件上双向绑定多个值!

3、常见错误和调试点

问题原因
父组件数据不更新子组件没有 emit update:modelValue
输入后无效子组件没绑定 :value
事件名拼错必须是 update:modelValue
未声明 propsmodelValue 没在 defineProps 中注册

Vue 3.4+ 中的新能力 —— 自定义组件支持 v-model 修饰符,也就是:

如何让一个组件能够识别、响应并处理像 .trim.capitalize 这样的 v-model 修饰符,并实现自定义行为。

这在实际开发中非常有用,比如封装输入组件时自动格式化大小写、过滤空格、处理数字等。

1、 什么是 v-model 修饰符?

Vue 在 v-model 上原生支持的修饰符包括:

修饰符功能
.trim去除字符串前后空格
.number将输入转换为数字
.lazy延迟到 change 时才更新

<input v-model.trim="username" />

在 Vue 原生组件中,这些由 Vue 自动处理。但在自定义组件中,我们需要显式实现支持这些修饰符。

 2、核心 API:defineModel() 和修饰符解构

const [model, modifiers] = defineModel()

  • model 是一个响应式引用,类似于以前的 modelValue

  • modifiers 是一个对象,包含当前 v-model 所使用的所有修饰符

3、基于修饰符处理数据:使用 get / set 转换函数

你可以为 defineModel() 提供 get / set 选项: 

const [model, modifiers] = defineModel({
  set(value) {
    if (modifiers.capitalize) {
      return value.charAt(0).toUpperCase() + value.slice(1)
    }
    return value
  }
})
  • 每次外部 v-model 设置值进来时,会通过 set() 处理;

  • 你可以在里面根据 modifiers 条件来转化数据,比如大小写转换、过滤等。

 使用示例

父组件:
<MyInput v-model.capitalize="username" />


子组件 MyInput.vue:
<script setup>
const [model, modifiers] = defineModel({
  set(value) {
    if (modifiers.capitalize && typeof value === 'string') {
      return value.charAt(0).toUpperCase() + value.slice(1)
    }
    return value
  }
})
</script>

<template>
  <input v-model="model" />
</template>
  • 用户输入 john

  • model 会变成 John 并 emit 给父组件;

  • 父组件中的 username 自动更新为 John

四、透传

Vue 的透传(Attribute Inheritance / 透传属性),是指当你在使用一个组件时,传递给它的非 prop 属性(未在 props 中声明的属性),会自动或手动传递到子组件内部的某个元素上

 1、什么是透传属性?

透传属性指的是父组件向子组件传递的、子组件没有声明为 props 的属性

Vue 会将这些属性自动收集到 $attrs,你可以选择是否让它们传递到组件的某个元素上。

 最常见的例子就是 classstyle 和 id

示例说明: 

<!-- 父组件 -->
<MyButton class="primary" id="submit-btn" />
如果 MyButton 组件内部没有声明 class 或 id 为 props:
<script setup>
defineProps([]) // 不接收任何 props
</script>

<template>
  <button>点击我</button>
</template>

2、默认行为(单根元素):

  • Vue 会自动把 class="primary"id="submit-btn" 透传到 <button>

 结果 DOM 渲染为:<button class="primary" id="submit-btn">点击我</button>

3、$attrs 的作用

$attrs 是一个包含所有未被声明为 props 的属性和事件监听器的对象。

import { useAttrs } from 'vue'
const attrs = useAttrs()

它是一个响应式只读对象,你可以:

  • 检查有哪些透传属性;

  • 将它们手动绑定到内部元素上。

4、对 class 和 style 的合并

如果一个子组件的根元素已经有了 class 或 style attribute,它会和从父组件上继承的值合并。

同样的规则也适用于 v-on 事件监听器: 如果原生 button 元素自身也通过 v-on 绑定了一个事件监听器,则这个监听器和从父组件继承的监听器都会被触发。

5、深层组件继承

当你的组件根节点是另一个组件时(比如你的组件是一个“代理组件”),透传属性是如何继续传递的,以及哪些会被消费、哪些会被继续传下去。

 根节点是另一个组件的情况

<!-- MyButton.vue -->
<template>
  <BaseButton />
</template>

在这个设计中:

  • <MyButton /> 是一个外壳(代理)组件;

  • 它的根节点直接渲染了另一个子组件 <BaseButton />

  • 用户在使用 <MyButton /> 时传入的一些属性或事件,可能需要“透传”到底层的 <BaseButton />

6、透传机制如何处理?

Vue 的透传属性机制如下:

类型是否自动透传给 BaseButton?说明
未声明的属性(如 classid✅ 会Vue 自动透传到 <BaseButton />
未声明的事件监听器(如 @click✅ 会会被当作 $attrs 继续传递下去
声明过的 props(在 MyButton 中声明)❌ 不会MyButton 自己“消费”
声明过的 emits(如 defineEmits(['submit'])❌ 不会MyButton 自己监听,Vue 认为你要手动处理

 如何将消费过的 props/emits 显式“再透传”?

 Props 手动传下去:<BaseButton :type="type" />

Emits 手动再触发:<BaseButton @submit="emit('submit', $event)" />
这样你才能让 <MyButton> 代理 typesubmit 的行为,使它们继续在 <BaseButton> 上生效。

7、禁用 Attributes 继承

如果你不想要一个组件自动地继承 attribute,你可以在组件选项中设置 inheritAttrs: false

 从 3.3 开始你也可以直接在 <script setup> 中使用defineOptions

<script setup>
defineOptions({
  inheritAttrs: false
})
// ...setup 逻辑
</script>

现在我们要再次使用一下 <MyButton> 组件例子。有时候我们可能为了样式,需要在 <button> 元素外包装一层 <div>

<div class="btn-wrapper">
  <button class="btn" v-bind="$attrs">Click Me</button>
</div>

我们想要所有像 class 和 v-on 监听器这样的透传 attribute 都应用在内部的 <button> 上而不是外层的 <div> 上。我们可以通过设定 inheritAttrs: false 和使用 v-bind="$attrs" 来实现:

<div class="btn-wrapper">
  <button class="btn" v-bind="$attrs">Click Me</button>
</div> 

8、多根节点的 Attributes 继承

和单根节点组件有所不同,有着多个根节点的组件没有自动 attribute 透传行为。

<template>
  <header>头部</header>
  <footer>底部</footer>
</template>
这时如果父组件这样使用:
<MyLayout class="layout" />
❌ class="layout" 将不会被传递给 header 或 footer,因为 Vue:

无法判断应该透传到哪一个根节点;

所以默认放弃透传,你必须手动绑定。

 如果需要,你可以在 <script setup> 中使用 useAttrs() API 来访问一个组件的所有透传 attribute:

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

const attrs = useAttrs()
</script>

 需要注意的是,虽然这里的 attrs 对象总是反映为最新的透传 attribute,但它并不是响应式的 (考虑到性能因素)。你不能通过侦听器去监听它的变化。如果你需要响应性,可以使用 prop。或者你也可以使用 onUpdated() 使得在每次更新时结合最新的 attrs 执行副作用。

五、插槽

 1、什么是插槽(slot)?

插槽是 Vue 提供的一种机制,用于 父组件将模板内容“注入”到子组件内部指定位置

可以把插槽看作是“组件内部预留的占位区域”,让父组件在使用时可以自定义渲染内容

 2、默认插槽

最基础的插槽用法,只使用 <slot /> 标签。

子组件:
<!-- MyCard.vue -->
<template>
  <div class="card">
    <slot /> <!-- 插槽位置 -->
  </div>
</template>
父组件使用:
<MyCard>
  <p>我是通过插槽传进去的内容</p>
</MyCard>
效果:
最终渲染相当于:
<div class="card">
  <p>我是通过插槽传进去的内容</p>
</div>

3、具名插槽

 你需要插入多个不同区域的内容时,可以使用具名插槽

子组件:
<template>
  <header>
    <slot name="header" />
  </header>
  <main>
    <slot />
  </main>
  <footer>
    <slot name="footer" />
  </footer>
</template>
 父组件:
<MyLayout>
  <template #header>
    <h1>顶部标题</h1>
  </template>
  <p>中间内容</p>
  <template #footer>
    <small>底部版权</small>
  </template>
</MyLayout>
#header 是 slot name="header" 的内容;
默认插槽(没有 name)会插到 <slot />。
✅ 语法糖 #xxx 相当于 v-slot:xxx。

 4、条件插槽

 Vue 提供了 $slots 对象,用来访问传入的插槽内容。
你可以用它判断插槽是否存在,从而决定是否渲染某段结构。

场景:

比如你想封装一个 Card 组件:

  • 如果传入了 header 插槽,就显示 .card-header

  • 否则,不渲染这个 DOM 元素;

  • 同样的逻辑适用于 footer 和默认插槽。

<template>
  <div class="card">
    <div v-if="$slots.header" class="card-header">
      <slot name="header" />
    </div>
    
    <div v-if="$slots.default" class="card-content">
      <slot />
    </div>
    
    <div v-if="$slots.footer" class="card-footer">
      <slot name="footer" />
    </div>
  </div>
</template>

 解释:
$slots.header 是一个函数,只要插槽传入了内容,就返回真;
v-if="$slots.header" 只在插槽内容存在时才渲染对应的包装结构;
这样可以避免不必要的 DOM 结构,组件更轻量、更智能。

补充知识:

  • $slots 是 Vue 运行时提供的插槽访问 API;

  • 你也可以访问作用域插槽内容(如 $slots.default());

  • 不推荐在 <script setup> 中直接访问 $slots,应使用 useSlots()

 5、动态插槽名

Vue 支持在 v-slot 或其缩写 # 中使用动态参数,让你可以根据变量名选择插入哪个具名插槽

就像在指令中用 v-bind:[attr] 动态绑定属性名,这里是动态绑定插槽名。

<base-layout>
  <template v-slot:[dynamicSlotName]>
    <p>插入动态插槽的内容</p>
  </template>

  <!-- 等价的缩写写法 -->
  <template #[dynamicSlotName]>
    <p>插入动态插槽的内容</p>
  </template>
</base-layout>
dynamicSlotName 是你在 <script setup> 或 data 中定义的变量,如:

const dynamicSlotName = 'main' // 动态决定插槽名
如果子组件定义了 <slot name="main" />,这个内容就会插入那里;

这是动态选择插槽的一种方式,可以根据业务逻辑插入不同位置。

 🔒 动态插槽名的语法限制:
表达式必须是字符串或能转换为字符串的值;
不能嵌套复杂表达式(如 a.b.c + '-slot' 需要先赋值为变量);
不能用于默认插槽(因为默认插槽没有名字);

6、作用域插槽

 插槽内容可以访问到父组件的数据作用域,因为插槽内容本身是在父组件模板中定义的。插槽内容无法访问子组件的数据。Vue 模板中的表达式只能访问其定义时所处的作用域

 如果你希望子组件向插槽暴露一些数据,让父组件在插入内容时可以动态使用这些数据,就要用到作用域插槽

 子组件:
<!-- ListRenderer.vue -->
<template>
  <ul>
    <li v-for="(item, index) in items" :key="index">
      <slot :item="item" :index="index" />
    </li>
  </ul>
</template>
<script setup>
defineProps({
  items: Array
})
</script>
父组件使用:
<ListRenderer :items="['Vue', 'React', 'Svelte']">
  <template #default="{ item, index }">
    {{ index + 1 }} - {{ item.toUpperCase() }}
  </template>
</ListRenderer>

子组件使用 slot 并暴露 :item、:index 数据;
父组件通过解构作用域插槽参数使用这些数据;
父组件可以完全控制每一项如何渲染(定制渲染逻辑)。

 子组件传入插槽的 props 作为了 v-slot 指令的值,可以在插槽内的表达式中访问。

<MyComponent v-slot="{ text, count }"> {{ text }} {{ count }} </MyComponent> 父组件也可以通过v-slot指令得到子组件的值

作用域插槽的应用场景总结

应用场景说明
高级可定制的组件组件封装逻辑,视图交给外部
列表、表格、分页等通用结构可插入任意项结构
表单、弹窗、布局区域定制分区插槽
响应式逻辑封装(鼠标、拖拽)无渲染组件,纯逻辑暴露数据

### 实现 Vue 3 组件间通信的方式 #### 使用 Props 和 Events 进行父子组件间的值 在 Vue 3 中,父组件可以通过 `props` 向子组件递数据,在子组件中接收并处理这些属性。当需要更新父组件的数据时,则可以利用 `$emit()` 函数触发自定义事件来通知父级。 ```html <!-- ParentComponent.vue --> <template> <child-component :message="parentMsg" @change-message="handleChange"></child-component> </template> <script> import ChildComponent from &#39;./ChildComponent.vue&#39;; export default { components: { ChildComponent }, data() { return { parentMsg: &#39;Hello from Parent&#39; }; }, methods: { handleChange(newVal) { this.parentMsg = newVal; } } }; </script> ``` ```html <!-- ChildComponent.vue --> <template> <p>{{ message }}</p> <button @click="sendMessageToParent">Change Message</button> </template> <script> export default { props: [&#39;message&#39;], emits: [&#39;change-message&#39;], // 明确声明预期接收到的事件名称 methods: { sendMessageToParent() { this.$emit(&#39;change-message&#39;, &#39;New message from child&#39;); } } } </script> ``` 此方法适用于简单的单向或双向绑定场景[^1]。 #### 利用 Provide/Inject API 构建祖孙关系链路中的依赖注入模式 对于多层嵌套结构下的跨层级通讯需求,Vue 提供了 provide/inject 特性用于简化这种情况下状态管理逻辑: ```javascript // App.vue export default { setup() { const themeColor = ref(&#39;#ff0&#39;); provide(&#39;theme-color&#39;, computed(() => themeColor.value)); function toggleTheme() { themeColor.value = themeColor.value === &#39;#fff&#39; ? &#39;#000&#39; : &#39;#fff&#39;; } return { toggleTheme }; } } // SomeGrandChildComponent.vue setup(props, context) { let color = inject(&#39;theme-color&#39;); console.log(color); // 获取祖先提供的主题颜色 watchEffect(() => { document.body.style.backgroundColor = color; }); } ``` 这种方式使得深层后代可以直接访问高层级的状态而无需逐层参数。 #### EventBus 或者 Vuex Store 解决非父子组件之间的消息广播机制 针对完全无关联甚至平行存在的两个及以上视图模块间的信息交换难题,官方建议采用全局事件总线(Event Bus)或者更复杂的集中式存储库(Vuex store),前者适合小型项目快速开发阶段使用;后者则更适合大型应用长期维护考虑。 以下是基于简单 Event Bus 的例子[^2]: ```javascript // main.js 初始化创建个空实例作为中介桥梁 const bus = new Vue(); new Vue({ el: &#39;#app&#39;, ... }); // Sender.vue 发送端发送特定类型的信号携带有效载荷给监听器们 bus.$emit(&#39;some-event-name&#39;, payload); // Receiver.vue 接收端订阅感兴趣的消息类型等待回调执行业务操作 bus.$on(&#39;some-event-name&#39;, handler); ``` 以上三种方案覆盖了大部分日常遇到的实际应用场景,开发者可以根据具体项目的规模和技术栈特点选取最合适的种或组合运用它们完成高效优雅的组件化编程工作[^3]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值