简历写精通 Vue 却频频挂面试?这篇真题救你 offer(四)

四季读书网 4 0
简历写精通 Vue 却频频挂面试?这篇真题救你 offer(四)
简历写精通 Vue 却频频挂面试?这篇真题救你 offer(四)-第1张图片-四季读书网

📋 目录

  • Q1. Vue 的核心是什么?对比 Vue 2 和 Vue 3 的响应式原理有什么区别

  • Q2. 什么是 MVVM 模式?Vue 是如何体现 MVVM 的?

  • Q3. Vue 实例的选项有哪些?(例如 data、computed、methods、watch、生命周期等)

  • Q4. v-if 和 v-show 有什么区别?分别适用于什么场景?

  • Q5. v-for 和 v-if 为什么不推荐一起使用?有什么替代方案?

  • Q6. Vue 中如何实现父子组件通信?(props + $emit / v-model)

  • Q7. 什么是插槽(Slot)?slot 和 slot-scope 在 Vue 2 和 Vue 3 中有什么区别?

  • Q8. computed 和 watch 有什么区别?各自的使用场景是什么?

  • Q9. 什么是 nextTick?它的实现原理是什么?

  • Q10. Vue 3 的 Composition API 和 Options API 有什么区别?

Q7. 什么是插槽(Slot)?slot 和 slot-scope 在 Vue 2 和 Vue 3 中有什么区别?

考点

插槽的概念、作用域插槽的原理、Vue 2 与 Vue 3 插槽写法的变化、以及插槽的实际应用场景。

答案

插槽(Slot) 是 Vue 中用于内容分发的机制,允许父组件向子组件的指定位置插入 HTML 内容,让子组件变得更加灵活和可复用。

插槽的核心价值

  • 内容由父组件决定:子组件只负责"占位"和"提供数据",具体渲染什么由使用它的父组件决定。

  • 复用公共结构:可以封装通用的"框架"(如对话框、表格、卡片),把可变部分用插槽暴露出去。

  • 保持组件封装性:子组件不依赖父组件的具体内容,实现解耦。

插槽的分类

1. 默认插槽

子组件用 <slot /> 声明一个"插槽位",父组件在使用时把内容放在子组件标签内部,内容会自动填充到子组件的 <slot /> 位置。可以给 <slot> 设置默认值(写在标签内的内容),当父组件不传内容时显示。

2. 具名插槽

子组件用 <slot name="header" /> 声明多个带名字的插槽位,父组件用 <template #header> 形式指定内容对应哪个插槽。常用于多区域布局(页头/内容/页脚)。

3. 作用域插槽

子组件在 <slot :prop="value"> 上绑定数据,父组件在 <template #default="{ prop }"> 中接收这个数据。作用域插槽让子组件的数据能传到父组件的插槽模板中,是封装可复用组件的关键能力。

4. 动态插槽名(Vue 3)

Vue 3 支持 #[dynamicName] 语法,插槽名可以是动态字符串。适合"根据配置决定插槽名"的场景。

Vue 2 vs Vue 3 插槽写法对比

  • 默认插槽:两种版本写法相同。

  • 具名插槽:声明侧写法相同(<slot name="header" />)。

  • 父组件使用侧

    • Vue 2:使用 slot="header" 和 slot-scope="{ item }" 写在子元素上。

    • Vue 3:使用 <template #header="{ item }">,语法更简洁、模板更整洁。

  • 动态插槽名:Vue 2 不支持,Vue 3 支持 #[dynamicName]

  • 废弃语法:Vue 3 中 slot 和 slot-scope 属性已完全移除,必须用 v-slot 或 # 简写。

实际项目中的插槽应用

场景:通用表格组件 Table.vue

子组件 Table 提供通用的表格框架(表头、表格体、列定义),把"每个单元格怎么渲染"交给父组件用插槽决定:

  • 表格组件接受 columns 配置和 data 数据。

  • 每个单元格用 <slot :name="column.key" :row="row" :column="column" :index="index"> 声明作用域插槽。

  • 默认插槽内容是直接显示原始值。

  • 父组件可以自定义"状态列"显示带颜色的 badge、"操作列"放编辑/删除按钮、"金额列"格式化显示。

  • 父组件可以自定义表头(#header 插槽)。

这样表格组件完全不关心业务逻辑,所有展示规则由父组件决定,可以复用到任何数据列表场景。

知识点详解

作用域插槽的本质

作用域插槽的核心是子组件渲染时把数据传给父组件的插槽模板

<!-- 子组件 Table.vue -->
<template>
<table>
<tr v-for="row in data" :key="row.id">
<td>
<!-- 把 row、column、index 传给父组件的插槽模板 -->
<slot:row="row" :column="column" :index="index">
          {{ row[column.key] }} <!-- 默认值:直接显示 -->
</slot>
</td>
</tr>
</table>
</template>
<!-- 父组件使用 -->
<Table :columns="columns" :data="users">
<!-- 父组件的插槽模板能访问子组件传入的数据 -->
<template #status="{ row }">
<span :class="['badge', `badge-${row.status}`]">
      {{ statusText[row.status] }}
</span>
</template>
</Table>

实现原理:父组件在解析插槽模板时,模板被编译成一个"插槽函数",子组件在调用 <slot :row="row"> 时把这个函数执行并传入数据。

Vue 2 vs Vue 3 写法对比

<!-- Vue 2:slot 和 slot-scope 写在子元素上 -->
<ChildComponent>
<div slot="header" slot-scope="{ user }">
    {{ user.name }}
</div>
<div slot-scope="{ items }">
    {{ items.length }} 个项目
</div>
</ChildComponent>

<!-- Vue 3:必须写在 <template> 上,更清晰 -->
<ChildComponent>
<template #header="{ user }">
    {{ user.name }}
</template>
<template #default="{ items }">
    {{ items.length }} 个项目
</template>
</ChildComponent>

v-slot 简写

<!-- 完整写法 -->
<ChildComponent v-slot:header="{ user }">
  {{ user.name }}
</ChildComponent>

<!-- 简写 -->
<ChildComponent #header="{ user }">
  {{ user.name }}
</ChildComponent>

<!-- 默认插槽的简写 -->
<ChildComponent v-slot="{ item }">
  {{ item.name }}
</ChildComponent>
<!-- 等价于 -->
<ChildComponent #default="{ item }">
  {{ item.name }}
</ChildComponent>

默认插槽的简写规则

<!-- ✅ 只有一个默认插槽时,可以省略 template -->
<MyButton>点击我</MyButton>

<!-- ❌ 存在具名插槽时,必须用 template -->
<Layout>
<template #header>标题</template>
<main>内容</main><!-- 这个是默认插槽,但与其他具名插槽共存时建议也用 template -->
</Layout>

动态插槽名(Vue 3)

<!-- 插槽名是动态的 -->
<DynamicComponent>
<template #[dynamicSlotName]>
    内容
</template>
</DynamicComponent>
exportdefault {
data() {
return {
dynamicSlotName:  'header'// 可以动态切换
    };
  }
};

通用表格组件完整示例

<!-- Table.vue 子组件 -->
<template>
<table class="table">
<thead>
<tr>
<th v-for="col in columns" :key="col.key">
<!-- 表头插槽 -->
<slot name="header" :column="col">
            {{ col.label }}
</slot>
</th>
</tr>
</thead>
<tbody>
<tr v-for="(row, index) in data" :key="row.id">
<t dv-for="col in columns" :key="col.key">
<!-- 单元格插槽,传递行数据 -->
<slot
:name="col.key"
:row="row"
:column="col"
:index="index"
:value="row[col.key]"
>
            {{ row[col.key] }}
</slot>
</td>
</tr>
</tbody>
</table>
</template>
<!-- 父组件使用 -->
<Table :columns="columns" :data="userList">
<template #status="{ value }">
<span :class="['status', `status-${value}`]">
      {{ statusText[value] }}
</span>
</template>

<template #actions="{ row }">
<button @click="edit(row.id)">编辑</button>
<button @click="remove(row.id)">删除</button>
</template>

<template #amount="{ value }">
    ¥{{ value.toFixed(2) }}
</template>
</Table>

Q8. computed 和 watch 有什么区别?各自的使用场景是什么?

考点

computed 与 watch 的本质区别、缓存机制、异步处理、以及实际场景的选择。

答案

computed(计算属性)和 watch(侦听器)虽然都能"响应数据变化",但本质不同,使用场景也不同

核心区别

  • computed 是"派生数据":从已有数据计算出新数据,有缓存(依赖不变多次访问只算一次),必须有返回值同步执行

  • watch 是"监听副作用":监听数据变化后执行一段逻辑(请求接口、修改其他状态、弹窗提示等),无缓存(每次数据变化都执行),不一定有返回值可以执行异步

为什么 computed 有缓存

computed 会追踪它依赖的所有响应式数据,并记录"上次计算的依赖版本"。当模板或其他地方访问 computed 时,Vue 会先检查依赖是否变化,没变化就直接返回缓存值,变化了才重新计算。这意味着 computed 不能在内部执行异步,因为异步返回的值不会进入缓存机制。

为什么 watch 没有缓存

watch 监听的数据每次变化都会触发回调,没有"依赖版本"的概念。这虽然开销大,但允许执行异步操作(比如防抖搜索、发起 API 请求),适合处理副作用。

computed 适用场景

  • 从已有数据派生新数据:原价+折扣 → 折后价、姓+名 → 全名、列表+筛选条件 → 过滤后的列表。

  • 模板中需要多次使用的复杂表达式:把 user.firstName + user.lastName 抽成 fullName

  • 依赖其他 computed 的派生数据:可以多个 computed 串联。

  • 需要 setter 双向绑定的场景:computed 支持 get/set 形式,可以做成"可写"的派生属性(如格式化全名反写回姓/名)。

watch 适用场景

  • 异步操作:搜索关键词变化 → 发起搜索请求(需要 debounce)。

  • 数据变化时执行副作用:用户名变化 → 验证是否被占用、保存到 localStorage。

  • 监听路由变化:路由参数变化 → 重新加载数据。

  • 深度监听对象/数组:开启 deep: true 监听对象内部属性变化。

  • 立即执行一次:开启 immediate: true 在组件创建时就执行一次(如根据初始 ID 加载数据)。

computed vs watch 对比

  • 本质:computed 是计算属性,watch 是观察副作用。

  • 返回值:computed 必须返回,watch 不需要。

  • 缓存:computed 有缓存,watch 没有。

  • 异步:computed 不支持,watch 支持。

  • 使用频率:computed 更常用(绝大多数派生场景),watch 只在需要副作用时用。

实际项目中的最佳实践

  • 能用 computed 就不用 watch:computed 语义更清晰、性能更好(缓存)。

  • watch 处理异步或副作用:搜索防抖、表单校验、保存草稿。

  • 避免在 watch 中直接修改被监听的数据:可能造成无限循环。

  • 对象/数组监听加 deep:默认 watch 不会深度监听对象内部属性。

  • 需要立即执行加 immediate:否则 watch 只在数据变化时触发。

知识点详解

计算属性的"惰性求值"

const computed( () => {
console.log('计算');
return 1+1;
});

// 此时不会执行 '计算',因为没有访问
a// 第一次访问:执行 '计算',返回 2 并缓存
a// 第二次访问:依赖未变,直接返回缓存,不再执行 '计算'
a.value// 依赖未变,返回缓存

惰性的价值:如果计算逻辑很重(比如深度遍历大数组),不访问就不算,节省性能。

计算属性的 getter / setter

export default {
data() {
return { firstName'张'lastName'三' };
  },
computed: {
// ✅ 只有 getter(只读派生)
fullName() {
return this.firstName this.lastName;
    },

// ✅ 有 getter 和 setter(可写派生)
fullNameWithSetter: {
get() {
return `${this.firstName}${this.lastName}`;
      },
set(value) {
const [firstlast]  value.split(' ');
this.firstName first;
this.lastName last || '';
      }
    }
  }
};
// v-model 绑定可写 computed
<input v-model="fullNameWithSetter"/>
// 用户输入 "李 四" 时,setter 把 firstName 改为 "李",lastName 改为 "四"

watch 的 deep 监听

export default {
data() {
return {
user: { name'张三'info: { age25 } }
    };
  },
watch: {
// ❌ 浅监听:user 对象引用变了才触发
user(newValoldVal) {
console.log('变化了');
    },
// ❌ 仍然浅监听:user.name 是值类型可以直接监听
'user.name'(newVal) {
console.log('姓名变化了');
    },
// ✅ 深度监听:user 内部任何属性变化都触发
user: {
handler(newVal) {
console.log('user 内部变化了');
      },
deeptrue
    }
  }
};

deep 的实现:Vue 内部会递归遍历被监听对象的每个属性,逐个建立依赖。开销大,应精确指定要监听的属性

watch 的 immediate 选项

export default {
data() { return { userIdnulluserInfonull }; },
watch: {
// ❌ 默认只在数据变化时触发,初始 userId=null 时不会执行
userId(id) {
this.fetchUser(id);
    },

// ✅ immediate: true 组件创建时立即执行一次
userId: {
handler(id) {
this.fetchUser(id);
      },
immediatetrue
    }
  },
created() {
this.userId =123// 此时会立即触发 fetchUser(123)
  }
};

watchEffect(Vue 3)

import { watchEffectref } from'vue';

const count ref(0);
const message ref('hello');

// 自动追踪函数内访问的所有响应式数据
watchEffect(() => {
console.log(count.valuemessage.value);
// count 或 message 任一变化都会重新执行
});

count.value=1// 触发执行
message.value='world'// 触发执行

vs watch

  • watch 显式指定监听源(watch(count, cb)),依赖清晰。

  • watchEffect 自动追踪,代码更简洁,但依赖不明确。

  • 团队中建议优先用 watch,副作用明确时用 watchEffect

不要在 watch 中修改被监听的数据

// ❌ 错误:watch 内部又修改了 count,count 又触发 watch,无限循环
watch: {
count(newVal) {
this.count newVal+1;
  }
}

// ✅ 正确:用中间变量
watch: {
count(newVal) {
this.nextCount newVal+1// 不修改被监听的数据
  }
}

防抖搜索的 watch 用法

export default {
data() {
return {
keyword'',
results: [],
debounceTimernull
    };
  },
watch: {
keyword(newKeyword) {
// ✅ watch 适合做防抖
if (this.debounceTimerclearTimeout(this.debounceTimer);
this.debounceTimer setTimeout(async () => {
const res await fetch(`/api/search?q=${newKeyword}`);
this.results awaitres.json();
      }, 300);
    }
  }
};

抱歉,评论功能暂时关闭!