Skip to content

模板语法与组件通信

✏️Vue 的模板语法是其声明式渲染的核心,而组件通信则是构建可扩展应用的基础。这两者都是 Vue 面试中的绝对高频考点。

1. 模板语法深入解析

Vue 的指令(Directives)是带有 v- 前缀的特殊属性,用于在渲染的 DOM 上应用响应式行为。

v-bind (缩写 :) - 动态属性绑定

用于动态地绑定一个或多个 HTML 属性。

vue
<script setup>
import { ref, reactive } from 'vue';

const imageUrl = ref('logo.png');
const imageAlt = ref('Company Logo');
const isDisabled = ref(true);

// 使用对象一次性绑定多个属性
const elementAttributes = reactive({
  id: 'unique-element',
  'data-index': 123
});
</script>

<template>
  <!-- 基础用法 -->
  <img :src="imageUrl" :alt="imageAlt" />

  <!-- 绑定布尔值属性 -->
  <button :disabled="isDisabled">Click Me</button>

  <!-- 绑定一个包含多个属性的对象 -->
  <div v-bind="elementAttributes"></div>
</template>

v-model - 双向数据绑定

v-model 是一个语法糖,用于在表单输入元素或自定义组件上创建双向数据绑定。它会根据元素类型自动选择正确的方式来更新值。

核心原理: :value + @input 事件的结合。

修饰符:

  • .lazy: 将 input 事件同步改为 change 事件同步。

  • .number: 将输入值自动转为数字。

  • .trim: 自动过滤用户输入的首尾空白字符。

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

const message = ref('');
const age = ref(0);
const username = ref('');
</script>

<template>
  <!-- 基础用法 -->
  <input v-model="message" placeholder="输入内容..." />
  <p>Message is: {{ message }}</p>

  <!-- .number 修饰符 -->
  <input v-model.number="age" type="number" />
  
  <!-- .trim 修饰符 -->
  <input v-model.trim="username" />
</template>

v-for - 列表渲染

用于基于一个数组来渲染一个列表。

  • 关键: 必须使用 key 属性为每个节点提供一个唯一的标识,以帮助 Vue 高效地更新 DOM。

  • 适用范围: 可以遍历数组、对象、数字和字符串。

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

const items = ref([
  { id: 1, name: 'Apple' },
  { id: 2, name: 'Banana' },
  { id: 3, name: 'Cherry' }
]);
</script>

<template>
  <ul>
    <!-- 遍历数组 -->
    <li v-for="(item, index) in items" :key="item.id">
      {{ index }} - {{ item.name }}
    </li>
  </ul>
</template>

v-if / v-show - 条件渲染

用于根据条件决定是否渲染或显示一个元素。

  • v-if (真正的条件渲染):

    • 当条件为 false 时,元素及其子组件会被销毁和从 DOM 中移除。

    • 切换开销较高,但初始渲染开销较低(如果条件为 false)。

    • 支持与 v-elsev-else-if 配合使用。

  • v-show (基于 CSS 的切换):

    • 元素始终会被渲染到 DOM 中。

    • 通过切换元素的 CSS display 属性来控制其显示和隐藏。

    • 切换开销较低,但初始渲染开销较高。

      使用原则:

  • 频繁切换:使用 v-show

  • 运行时条件很少改变:使用 v-if

vue
<script setup>
import { ref } from 'vue';
const isLoggedIn = ref(true);
const showDetails = ref(false);
</script>

<template>
  <!-- v-if 示例 -->
  <div v-if="isLoggedIn">
    Welcome back, User!
  </div>
  <div v-else>
    Please log in.
  </div>

  <!-- v-show 示例 -->
  <button @click="showDetails = !showDetails">Toggle Details</button>
  <div v-show="showDetails">
    This is a detail panel.
  </div>
</template>

2. 组件通信模式详解

父子通信:props / emit / slots

这是最常用和最基础的通信方式。

  • props (父 -> 子): 父组件通过属性将数据向下传递给子组件。

  • emit (子 -> 父): 子组件通过触发事件将信息发送给父组件。

  • slots (父 -> 子内容分发): 父组件可以将模板片段(内容)插入到子组件指定的位置。

父组件 ParentComponent.vue

vue
<script setup>
import { ref } from 'vue';
import UserCard from './UserCard.vue';

const user = ref({ id: 1, name: 'Alice' });

function handleUpdate(newName) {
  user.value.name = newName;
  alert(`User name updated to: ${newName}`);
}
</script>

<template>
  <UserCard :user-name="user.name" @name-updated="handleUpdate">
    <!-- 将内容插入到子组件的默认插槽中 -->
    <p>This is some extra information about the user.</p>
  </UserCard>
</template>

子组件 UserCard.vue

vue
<script setup>
// 1. 定义接收的 props
const props = defineProps({
  userName: {
    type: String,
    required: true
  }
});

// 2. 定义可以触发的事件
const emit = defineEmits(['name-updated']);

function updateUser() {
  const newName = props.userName + '!';
  // 3. 触发事件,将数据传递给父组件
  emit('name-updated', newName);
}
</script>

<template>
  <div class="card">
    <h3>{{ userName }}</h3>
    <!-- 4. 渲染父组件传递过来的插槽内容 -->
    <slot></slot>
    <button @click="updateUser">Update Name</button>
  </div>
</template>

双向绑定:在自定义组件上使用 v-model

你可以在自己的组件上实现 v-model,这对于创建自定义表单控件尤其有用。

  • 约定: 组件接收一个名为 modelValue 的 prop,并通过 update:modelValue 事件来更新它。

    父组件 ParentForm.vue

vue
<script setup>
import { ref } from 'vue';
import CustomInput from './CustomInput.vue';

const searchText = ref('Initial Text');
</script>

<template>
  <CustomInput v-model="searchText" />
  <p>Current search text: {{ searchText }}</p>
</template>

子组件 CustomInput.vue

vue
<script setup>
defineProps(['modelValue']);
const emit = defineEmits(['update:modelValue']);

function onInput(event) {
  emit('update:modelValue', event.target.value);
}
</script>

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

跨层级通信:provide / inject

当需要从祖先组件向其所有后代组件传递数据时,使用 provideinject 可以避免逐层传递 props(即“prop drilling”)。

  • provide: 在祖先组件中提供数据或方法。

  • inject: 在任何后代组件中注入(接收)这些数据或方法。

祖先组件 App.vue

vue
<script setup>
import { ref, provide } from 'vue';
import DeepChild from './DeepChild.vue';

const theme = ref('light');
function toggleTheme() {
  theme.value = theme.value === 'light' ? 'dark' : 'light';
}

// 提供数据和方法给所有后代
provide('theme', theme);
provide('toggleTheme', toggleTheme);
</script>

<template>
  <div :class="theme">
    <DeepChild />
  </div>
</template>

后代组件 DeepChild.vue

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

// 注入来自祖先的数据和方法
const theme = inject('theme', 'light'); // 'light' 是默认值
const toggleTheme = inject('toggleTheme');
</script>

<template>
  <p>Current theme is: {{ theme }}</p>
  <button @click="toggleTheme">Toggle Theme</button>
</template>

3. 面试核心问题与最佳实践

Q1: "v-if 和 v-show 有什么区别,应该如何选择?"

  • 区别:

    • 原理: v-if 是真正的条件渲染,它会确保在切换过程中条件块内的事件监听器和子组件被适当地销毁和重建。v-show 只是简单地切换元素的 CSS display 属性。

    • 性能: v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。

  • 选择:

    • 如果需要频繁地切换,使用 v-show 性能更好。

    • 如果条件在运行时很少改变,或者初始为假时不需要渲染任何东西,使用 v-if 更合适。

Q2: "在 Vue 3 中,v-if 和 v-for 一起使用时,哪个优先级更高?"

  • 在 Vue 3 中,v-if 的优先级高于 v-for。这意味着 v-if 会先生效,此时它还无法访问到 v-for 作用域中的变量。

  • 最佳实践是避免将它们放在同一个元素上。推荐的做法是:

    1. 使用一个计算属性(computed)来预先过滤掉不需要显示的项,然后在过滤后的列表上使用 v-for

    2. v-for 移到 <template> 标签上,然后在内部的元素上使用 v-if

vue
<!-- 推荐:使用计算属性 -->
<div v-for="item in visibleItems" :key="item.id">{{ item.name }}</div>

<!-- 备选:使用 <template> 标签 -->
<template v-for="item in items" :key="item.id">
  <div v-if="item.isVisible">{{ item.name }}</div>
</template>

Q3: "除了 props/emit,你还知道哪些组件通信方式?"

  • v-model: 用于在父子组件间创建双向绑定,非常适合封装表单控件。

  • provide/inject: 用于跨越多层的祖先到后代的通信,可以有效解决“prop drilling”问题,常用于传递全局配置或主题信息。

  • 状态管理库 (Pinia/Vuex): 当多个组件需要共享和操作同一份复杂状态时,应使用集中的状态管理方案,以保证数据流的可预测性和可维护性。

  • ✅最佳实践

  1. 数据单向流动: 始终坚持父组件通过 props 向下传递数据,子组件通过 emit 事件通知父组件进行状态变更。子组件永远不应直接修改 props

  2. key 的重要性: 在使用 v-for 时,总是提供一个唯一的、稳定的 key 值(如 item.id),而不是使用 index,这对于性能优化和避免状态混淆至关重要。

  3. 选择合适的通信方式:

    • 父子: 默认使用 props/emit

    • 深层嵌套: 考虑 provide/inject

    • 兄弟或远亲: 提升状态到共同的父组件,或使用 Pinia

  4. 组件接口清晰: 为 props 提供明确的类型、默认值和校验规则。为 emits 做出明确的声明。这使得组件像一份清晰的 API 文档,易于理解和使用。

不知道说啥了很无语了