🔥前端面试必看:Vue 核心知识点保姆级整理,11 个章节 + 高频面试题全解析
从模板语法到响应式原理,从组件通信到生命周期——这一篇就够了。
为什么写这篇文章
最近准备面试,把 Vue 从基础到进阶的知识点重新梳理了一遍。翻遍网上的文章,要么太浅只讲 API,要么太深直接啃源码——对面试来说都不合适。
所以我把面试官爱问的点、容易踩的坑、以及高频面试题的标准答法全部整理成了这份笔记,每一节都附有代码示例和面试话术。
适合谁看?
- ✅ 正在准备前端面试的同学(每节末尾都有高频面试题精选)
- ✅ 学过 Vue 但知识点零散,想系统梳理一遍的人
- ✅ 正在从 Vue2 迁移到 Vue3 的开发者
阅读建议:全文约 1.5 万字,代码片段均可直接运行,建议收藏后配合自己的项目慢慢消化。文末附「知识点速查表」,面试前 10 分钟可速览一遍。
📑 目录
📌 附:面试知识点速查表 —— 建议面试当天早上扫一遍
1. Vue 概述
1.1 什么是 Vue
Vue 是一个用于构建用户界面的渐进式 JavaScript 框架。拆开看这五个关键词,每一个都是面试加分点:
- 渐进式:可以只用核心库写个小组件,也可以引入 Router / Pinia 全家桶做大型 SPA——学多少用多少,没有一步到位的心理负担
- 响应式:数据变化,视图自动更新——这是 Vue 最核心的魔法,也是面试重灾区
- 组件化:UI 拆成独立、可复用的组件,配合 SFC(单文件组件)组织代码
- 虚拟 DOM:通过 diff 算法批量、最小化地更新真实 DOM
- 单文件组件:
.vue文件把模板、逻辑、样式组织在一起,配合scoped实现样式隔离
1.2 Vue 2 vs Vue 3
这是面试第一问,必须烂熟于心:
| 特性 | Vue 2 | Vue 3 |
|---|---|---|
| 响应式原理 | Object.defineProperty | Proxy |
| API 风格 | Options API | Options API + Composition API |
| 生命周期 | beforeCreate / created | setup() |
| 根节点 | 单根节点 | 支持多根节点(Fragment) |
| 性能 | 较好 | 更好(Tree-shaking、编译优化) |
| TypeScript | 支持有限 | 原生支持 |
| 包体积 | 较大 | 更小(按需引入) |
| Teleport | 不支持 | 支持 |
| Suspense | 不支持 | 支持(实验性) |
1.3 创建 Vue 项目
# Vue 3 - 官方脚手架 create-vue
npm create vue@latest
# Vue 3 - Vite 模板(更轻量,推荐)
npm create vite@latest my-vue-app -- --template vue
# Vue 2 - Vue CLI(老项目)
vue create my-project
1.4 项目结构
一份清晰的项目结构,体现了你组织代码的能力,面试问到项目经验时可以主动提:
my-vue-app/
├── public/
│ └── index.html
├── src/
│ ├── assets/ # 静态资源
│ ├── components/ # 公共组件(低耦合、可复用)
│ ├── views/ # 页面组件(与路由对应)
│ ├── router/ # 路由配置
│ ├── store/ # 状态管理
│ ├── composables/ # 组合式函数(Vue3 逻辑复用)
│ ├── utils/ # 工具函数
│ ├── App.vue # 根组件
│ └── main.js # 入口文件
├── package.json
└── vite.config.js
💬 面试题精选
Q1:Vue 的核心特性有哪些?
答:
- 响应式数据绑定:数据变化自动更新视图
- 组件系统:UI 拆分为独立可复用的组件
- 虚拟 DOM:通过 diff 算法高效更新 DOM
- 指令系统:
v-if、v-for、v-bind等声明式指令简化开发 - 模板语法:基于 HTML 的模板语法,上手门槛低
- 渐进式框架:可以逐步引入,从简单的增强到完整的 SPA
💡 加分话术:如果面试官追问"为什么说 Vue 是渐进式的?",可以答:Vue 被设计为可以自底向上逐层应用——核心库只关注视图层,配合官方生态(Vue Router / Pinia / Vite)可以扩展成大型应用,甚至通过 Nuxt 做 SSR,各层可独立选用。
Q2:Vue 2 和 Vue 3 的主要区别?
答:
- 响应式:Vue 2 用
Object.defineProperty,Vue 3 用Proxy(能检测新增/删除属性、数组索引) - API:Vue 3 新增 Composition API,按逻辑关注点组织代码,更适合复用和大型项目
- 性能:Vue 3 有编译时优化(静态提升、Patch Flag)和 Tree-shaking 支持
- TypeScript:Vue 3 用 TypeScript 重写,类型推断更好
- Fragment:Vue 3 支持多根节点组件
- 新组件:Teleport(传送门)、Suspense(异步依赖处理)
2. 模板语法
模板语法是日常编码最高频的部分,面试里常作为"暖场题"出现——暖场题答不好,印象分直接扣。
2.1 文本插值
<template>
<!-- 文本插值 -->
<p>{{ message }}</p>
<!-- 支持表达式(注意:不支持语句,如 if) -->
<p>{{ ok ? '是' : '否' }}</p>
<p>{{ message.split('').reverse().join('') }}</p>
<p>{{ number + 1 }}</p>
<!-- v-once:一次性插值,之后数据变化不再更新 -->
<p v-once>{{ message }}</p>
<!-- v-html:输出原始 HTML(⚠️ 有 XSS 风险,见面试题) -->
<div v-html="rawHtml"></div>
<!-- v-text:等同于 {{ }},但会替换整个元素的文本内容 -->
<p v-text="message"></p>
</template>
2.2 属性绑定
<template>
<!-- v-bind 绑定属性(缩写 :) -->
<img :src="imageUrl" />
<!-- 动态属性名(2.6+) -->
<div :[attrName]="value"></div>
<!-- 布尔属性:值为 falsy 时移除属性 -->
<button :disabled="isDisabled">按钮</button>
<!-- 绑定对象:一次绑定多个属性 -->
<div v-bind="{ id: 'main', class: 'container' }"></div>
</template>
2.3 指令
<template>
<!-- 指令 = v- 前缀的特殊属性 -->
<p v-if="seen">可见</p>
<!-- 指令参数 -->
<a v-bind:href="url">链接</a>
<button v-on:click="handler">按钮</button>
<!-- 动态参数 -->
<a :[attributeName]="url">链接</a>
<button @[eventName]="handler">按钮</button>
<!-- 修饰符:以 . 开头的特殊后缀 -->
<form v-on:submit.prevent="onSubmit">...</form>
<input v-model.trim="message" />
</template>
💬 面试题精选
Q1:v-html 有什么风险?
答:v-html 会跳过转义,把内容当作原始 HTML 注入页面。如果渲染的是用户输入(比如评论区、富文本),攻击者可以注入 <script> 或带事件的标签窃取 Cookie、伪造请求,即 XSS(跨站脚本攻击)。
💡 面试官追问:如果业务必须渲染富文本怎么办?
答:前端用 DOMPurify 之类的库对 HTML 做消毒(sanitize)再渲染;同时服务端也必须做过滤——安全不能依赖单一层面的防护。
Q2:{{ }} 和 v-text 的区别?
答:
{{ }}插值可以和其他文本混排,如Hello, {{ name }}!v-text会替换整个元素的文本内容,灵活性差
实战建议:需要混排用插值;需要整体替换且想避免插值闪烁(v-cloak 问题)时可用 v-text。
Q3:v-once 的使用场景?
答:渲染含有大量静态内容的组件时,配合 v-once 可以让内容只渲染一次并跳过之后的更新,是一个免费的性能优化点。
3. 数据绑定与响应式
⚠️ 本节是面试最高频、最容易翻车的部分,建议逐行理解。
3.1 Vue 2 的响应式与它的"坑"
export default {
data() {
return {
message: 'Hello',
user: { name: 'John', age: 30 },
items: [1, 2, 3]
};
},
methods: {
// ✅ 修改已存在的属性:响应式
updateMessage() {
this.message = 'World';
},
// ❌ 新增属性:Vue 2 检测不到!视图不会更新
addPropertyWrong() {
this.user.email = 'john@example.com';
},
// ✅ 正确姿势:$set
addProperty() {
this.$set(this.user, 'email', 'john@example.com');
// 或者 Vue.set(this.user, 'email', 'john@example.com');
},
// 数组:以下 7 个变异方法才能触发更新
// push / pop / shift / unshift / splice / sort / reverse
updateArray() {
this.items.push(4); // ✅
// ❌ 直接改索引、改 length,都不会触发更新
// this.items[0] = 10;
// this.items.length = 0;
// ✅ 正确姿势
this.$set(this.items, 0, 10);
this.items.splice(0, 1, 10);
}
}
};
为什么 Vue 2 有这些限制? 因为 Object.defineProperty 只能劫持已经存在的属性,新增属性时没有 getter/setter 可拦,数组索引同理。这也是 Vue 3 换成 Proxy 的核心动机。
3.2 Vue 3 响应式:ref 与 reactive
import { ref, reactive, toRef, toRefs, shallowRef, readonly } from 'vue';
// ---------- ref:包装任意类型,基本类型首选 ----------
const count = ref(0);
console.log(count.value); // 注意:JS 中要用 .value
count.value++; // 修改也要 .value
// 模板中自动解包:{{ count }},无需 .value
// ---------- reactive:只能用于对象/数组 ----------
const state = reactive({ name: 'John', age: 30 });
state.name = 'Jane'; // 直接访问,无需 .value
// ⚠️ reactive 的局限:不能替换整个对象,否则丢失响应性
// let state = reactive({ count: 0 });
// state = reactive({ count: 1 }); // ❌ 断开响应
// ---------- toRef / toRefs:解构不丢响应性 ----------
const nameRef = toRef(state, 'name'); // 单个属性
const { name, age } = toRefs(state); // 批量解构,name.value 仍是响应式
// ---------- shallowRef / shallowReactive:浅层响应 ----------
const shallow = shallowRef({ nested: { count: 0 } });
// 只有 .value 整体替换才触发更新,嵌套属性变化不触发(性能优化手段)
// ---------- readonly:只读代理,防止意外修改 ----------
const readonlyState = readonly(state);
3.3 ref vs reactive:到底用哪个?
| 对比项 | ref | reactive |
|---|---|---|
| 支持类型 | 任意(基本类型靠它) | 仅对象/数组 |
| 访问方式 | JS 中需要 .value | 直接访问属性 |
| 替换整个对象 | ✅ 可以(x = ref(newObj)) | ❌ 丢失响应性 |
| 解构 | 配合 toRefs 保持响应 | 配合 toRefs 保持响应 |
选择口诀:基本类型必用 ref;对象类型看场景——需要整体替换用 ref,需要大量属性访问用 reactive。
💬 面试题精选
Q1:Vue 2 和 Vue 3 响应式原理的区别? ⭐必考题
答:
| 特性 | Vue 2(Object.defineProperty) | Vue 3(Proxy) |
|---|---|---|
| 新增属性 | ❌ 检测不到(需 $set) | ✅ 自动检测 |
| 删除属性 | ❌ 检测不到(需 $delete) | ✅ 自动检测 |
| 数组索引/length | ❌ 直接修改无效 | ✅ 自动检测 |
| 初始化性能 | 递归遍历所有属性 | 惰性代理,访问时才递归 |
| 兼容性 | 支持 IE11 | ❌ 不支持 IE |
💡 加分话术:
Proxy是劫持整个对象,能拦截 13 种操作(get/set/has/deleteProperty 等),而defineProperty只能逐个属性劫持,所以 Vue 2 对新增属性和数组索引无能为力;代价是Proxy无法 polyfill 到 IE。
Q2:为什么 ref 需要 .value 而 reactive 不需要?
答:ref 需要一个包装对象来容纳任意类型的值(基本类型无法被 Proxy 拦截),.value 就是这个包装对象的属性,通过它的 getter/setter 实现响应式。而 reactive 直接返回对象的 Proxy 代理,属性访问被代理拦截,无需中间层。
💡 补充:模板中
ref会自动解包是因为编译器做了处理,注意对象属性中的 ref 不会自动解包(如reactive({ count: ref(0) })里要state.count.value)。
Q3:解构 reactive 对象后如何保持响应性?
答:使用 toRefs:
// ❌ 直接解构:丢失响应性
const { name, age } = reactive({ name: 'John', age: 30 });
// ✅ 正确姿势
const state = reactive({ name: 'John', age: 30 });
const { name, age } = toRefs(state);
// name.value / age.value 均为响应式
4. 计算属性与侦听器
💡 经验之谈:开发中 80% 的场景用
computed就够了,watch只用于数据变化时需要"做点什么"(发请求、操作 DOM、联动其他数据)。
4.1 计算属性(Computed)
<script setup>
import { ref, computed } from 'vue';
const firstName = ref('John');
const lastName = ref('Doe');
// 只读计算属性
const fullName = computed(() => `${firstName.value} ${lastName.value}`);
// 可读可写计算属性
const fullNameRW = computed({
get() {
return `${firstName.value} ${lastName.value}`;
},
set(value) {
const [first, last] = value.split(' ');
firstName.value = first;
lastName.value = last;
}
});
</script>
4.2 计算属性 vs 方法:缓存是关键
computed: {
// ✅ 有缓存:message 不变时,多次访问不会重新计算
reversedMessage() {
return this.message.split('').reverse().join('');
}
},
methods: {
// ❌ 无缓存:每次调用都重新执行
reversedMessageMethod() {
return this.message.split('').reverse().join('');
}
}
4.3 侦听器(Watch)
<script setup>
import { ref, reactive, watch, watchEffect } from 'vue';
const question = ref('');
const user = reactive({ name: 'John', age: 30 });
// ① 侦听 ref
watch(question, (newVal, oldVal) => {
console.log(`问题从 "${oldVal}" 变成了 "${newVal}"`);
});
// ② 侦听多个源(数组形式)
watch([question, () => user.name], ([newQ, newName], [oldQ, oldName]) => {
console.log('任一源变化都会触发');
});
// ③ 侦听 reactive 对象:自动深度侦听
watch(user, (newVal) => console.log('user 变了', newVal));
// ④ 侦听 reactive 的某个属性:必须用 getter 函数!
watch(
() => user.name,
(newVal, oldVal) => console.log('name 变了'),
{ immediate: true } // 立即先执行一次
);
// ⑤ watchEffect:自动追踪回调里用到的响应式数据
watchEffect(() => {
console.log('name:', user.name); // 用到谁就追踪谁,无需声明
});
</script>
4.4 watch vs watchEffect
| 特性 | watch | watchEffect |
|---|---|---|
| 侦听源 | 需要显式指定 | 自动追踪(用到即追踪) |
| 旧值 | ✅ 能拿到 oldValue | ❌ 拿不到 |
| 执行时机 | 默认懒执行(变化才跑) | 定义时立即执行一次 |
| 适用场景 | 需要新旧值对比、精准控制 | 副作用函数(发请求、同步 localStorage) |
💬 面试题精选
Q1:计算属性和方法的区别?
答:核心区别是缓存。计算属性基于响应式依赖缓存结果,依赖不变时多次访问直接返回缓存;方法每次调用都重新执行。
💡 实战话术:模板中做格式化展示优先用 computed;事件回调、需要传参的逻辑用 methods。滥用方法做模板渲染在大列表场景是常见的性能瓶颈。
Q2:watch 和 watchEffect 的区别?
答:
watch需要显式指定侦听源,能拿到新旧值,默认懒执行watchEffect自动追踪回调中用到的响应式数据,立即执行一次,拿不到旧值
💡 记忆技巧:
watch是"盯着谁看",watchEffect是"用了谁盯谁"。
Q3:如何深度侦听对象?
答:
// Vue 2:deep 选项
watch: {
user: {
handler(newVal) {},
deep: true,
immediate: true
}
}
// Vue 3:侦听 reactive 对象默认就是深度的
watch(user, (newVal) => {});
// 侦听 ref 包裹的对象需要 { deep: true }
watch(userRef, (newVal) => {}, { deep: true });
⚠️ 注意:
deep: true会递归遍历对象,大对象上有性能开销,能精确到属性就精确到属性。
5. Class 与 Style 绑定
这节不难,但数组+对象混用的写法是日常开发的效率利器。
5.1 绑定 Class
<template>
<!-- 对象语法:键是类名,值是布尔(最常用) -->
<div :class="{ active: isActive, 'text-danger': hasError }"></div>
<!-- 数组语法 -->
<div :class="[activeClass, errorClass]"></div>
<!-- 数组 + 对象混用(三目 + 对象) -->
<div :class="[isActive ? activeClass : '', { 'text-danger': hasError }]"></div>
<!-- 对象语法:直接绑定 data 中的对象 -->
<div :class="classObject"></div>
<!-- 组件上的 class 会合并到根元素(Vue 3 多根节点时需显式继承) -->
<my-component class="baz" />
</template>
<script setup>
const classObject = { active: true, 'text-danger': false };
</script>
5.2 绑定 Style
<template>
<!-- 对象语法(推荐):camelCase 或 kebab-case 都可以 -->
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
<!-- 绑定对象 -->
<div :style="styleObject"></div>
<!-- 数组语法:合并多个样式对象,后者覆盖前者 -->
<div :style="[baseStyles, overridingStyles]"></div>
<!-- 值为数组:自动加浏览器前缀 -->
<div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div>
</template>
6. 条件渲染与列表渲染
6.1 条件渲染
<template>
<!-- v-if 家族:真正的条件渲染 -->
<div v-if="type === 'A'">A</div>
<div v-else-if="type === 'B'">B</div>
<div v-else>其他</div>
<!-- v-show:只是切 display -->
<div v-show="isVisible">始终会被渲染</div>
<!-- template 包裹多个元素,不产生多余 DOM -->
<template v-if="ok">
<h1>标题</h1>
<p>内容</p>
</template>
<!-- ⚠️ v-if 会复用相同元素,用 key 强制重建 -->
<template v-if="loginType === 'username'">
<input placeholder="用户名" key="username" />
</template>
<template v-else>
<input placeholder="邮箱" key="email" />
</template>
</template>
💡 上面最后这个例子很实用:不加
key时切换登录方式,输入框的内容会被复用保留(因为 Vue 认为还是同一个 input),加了不同key才会重新渲染。
6.2 列表渲染
<template>
<!-- 数组 -->
<ul>
<li v-for="(item, index) in items" :key="item.id">
{{ index }} - {{ item.name }}
</li>
</ul>
<!-- 对象 -->
<ul>
<li v-for="(value, key, index) in object" :key="key">
{{ index }}. {{ key }}: {{ value }}
</li>
</ul>
<!-- 整数范围 -->
<span v-for="n in 10" :key="n">{{ n }}</span>
</template>
<script setup>
import { ref, computed } from 'vue';
const todos = ref([
{ id: 1, text: 'Learn Vue', done: true },
{ id: 2, text: 'Build app', done: false }
]);
// ✅ 过滤逻辑放 computed,不要 v-for + v-if 混写
const activeTodos = computed(() => todos.value.filter(t => !t.done));
</script>
💬 面试题精选
Q1:v-if 和 v-show 的区别? ⭐高频
答:
| 特性 | v-if | v-show |
|---|---|---|
| 实现 | 真正的条件渲染(销毁/重建组件) | 仅切换 display: none |
| 初始开销 | 条件为 false 时不渲染(惰性) | 无论如何都渲染 |
| 切换开销 | 高(组件重建 + 可能的异步请求重发) | 低(纯 CSS) |
| 适用场景 | 运行时条件很少改变 | 频繁切换(如 Tab) |
💡 加分话术:
v-if切换时会触发组件的销毁/重建生命周期,如果里面是带请求的组件要留意重复请求;v-show初始渲染成本高但切换几乎零成本。
Q2:为什么 v-for 需要 key?用 index 做 key 有什么问题? ⭐必考
答:
key 是 Vue 判断节点身份的唯一标识。diff 算法通过 key 匹配新旧节点,从而复用 DOM、移动元素,而不是全部销毁重建。
用 index 做 key 的问题:
- 数据顺序变化(排序、插入、删除)时,key 与数据的对应关系错位
- Vue 会错误复用组件状态——比如输入框内容错乱、选中状态串位
- 性能反而更差(diff 无法识别"移动",只能原地更新)
正确做法:使用数据中的唯一 id 作为 key。
💡 加分话术:
key还能配合v-if强制重建组件(改变 key 值触发重新渲染),是实现"重置组件状态"的常用技巧。
Q3:v-for 和 v-if 为什么不建议一起使用?
答:
- Vue 2:
v-for优先级高于v-if,每次循环都执行判断,性能浪费 - Vue 3:
v-if优先级高于v-for,此时v-if中访问不到循环变量(会直接报错/为 undefined)
两种解决方案:
<!-- 方案一:computed 预先过滤(最推荐) -->
<li v-for="todo in activeTodos" :key="todo.id">...</li>
<!-- 方案二:template 包裹(循环和判断分离) -->
<template v-for="todo in todos" :key="todo.id">
<li v-if="todo.active">...</li>
</template>
7. 事件处理
7.1 事件监听全览
<template>
<!-- 内联处理器 -->
<button @click="count++">点击 {{ count }}</button>
<!-- 方法处理器(默认接收原生 event) -->
<button @click="greet">问候</button>
<!-- 传参 -->
<button @click="say('hello')">说你好</button>
<!-- 既要传参又要 event:用 $event -->
<button @click="warn('hello', $event)">警告</button>
<!-- 事件修饰符(重点掌握) -->
<a @click.stop="handler">阻止冒泡</a>
<a @click.prevent="handler">阻止默认行为</a>
<a @click.stop.prevent="handler">链式使用</a>
<a @click.self="handler">仅 event.target 为自身时触发</a>
<a @click.once="handler">只触发一次</a>
<div @scroll.passive="handler">被动模式(提升滚动性能)</div>
<!-- 按键修饰符 -->
<input @keyup.enter="submit" />
<input @keyup.esc="cancel" />
<input @keyup.ctrl.enter="submit" /> <!-- 组合键 -->
<!-- .exact:精确匹配,不多不少 -->
<button @click.ctrl.exact="handler">只按 Ctrl 时才触发</button>
<!-- 鼠标按键修饰符 -->
<div @click.right="handler">右键</div>
</template>
💬 面试题精选
Q1:Vue 事件修饰符有哪些?
答:
.stop:阻止事件冒泡.prevent:阻止默认行为(如表单提交、a 跳转).capture:改为捕获模式监听.self:仅当event.target是元素自身才触发(防冒泡误触).once:只触发一次.passive:告诉浏览器不调用preventDefault,移动端滚动性能优化的关键
💡 加分话术:
.passive的原理是浏览器对 passive listener 可以提前开始滚动而不用等待 JS 判断是否会 preventDefault,滚动场景(如无限列表)加上它能显著减少卡顿。
Q2:Vue 事件和原生 DOM 事件有什么区别?
答:
- Vue 事件是对原生 DOM 事件的封装,事件对象就是原生的(
$event) - Vue 组件卸载时会自动移除事件监听,避免内存泄漏
- 组件上直接
@click:Vue 3 默认透传到组件根元素(多个根节点时需在子组件用inheritAttrs: false+v-bind="$attrs"手动指定) - Vue 2 中组件原生事件需要
.native修饰符,Vue 3 已移除该机制
8. 表单处理
8.1 v-model 全类型用法
<template>
<!-- 文本 / 多行文本 -->
<input v-model="message" />
<textarea v-model="message"></textarea>
<!-- 单个复选框:布尔值 -->
<input type="checkbox" v-model="checked" />
<!-- 多个复选框:数组 -->
<input type="checkbox" v-model="checkedNames" value="Jack" />
<input type="checkbox" v-model="checkedNames" value="John" />
<!-- 单选:字符串 -->
<input type="radio" v-model="picked" value="One" />
<input type="radio" v-model="picked" value="Two" />
<!-- 单选下拉:字符串 -->
<select v-model="selected">
<option disabled value="">请选择</option>
<option>A</option><option>B</option>
</select>
<!-- 多选下拉:数组 -->
<select v-model="selectedList" multiple>
<option>A</option><option>B</option>
</select>
<!-- 三个实用修饰符 -->
<input v-model.trim="message" /> <!-- 去首尾空格 -->
<input v-model.number="age" /> <!-- 转数字(否则永远是字符串) -->
<input v-model.lazy="message" /> <!-- 失焦/change 时才更新(减少渲染次数) -->
</template>
8.2 v-model 的原理:它只是语法糖
<!-- 文本框上的 v-model 本质是 -->
<input :value="message" @input="message = $event.target.value" />
<!-- Vue 3 组件上的 v-model -->
<my-component v-model="message" />
<!-- 编译为: -->
<my-component :modelValue="message" @update:modelValue="message = $event" />
8.3 自定义组件实现 v-model(含自定义修饰符)
<script setup>
// 子组件
const props = defineProps({
modelValue: String,
modelModifiers: { default: () => ({}) } // 接收修饰符
});
const emit = defineEmits(['update:modelValue']);
function updateValue(event) {
let value = event.target.value;
// 自定义 .capitalize 修饰符的逻辑
if (props.modelModifiers.capitalize) {
value = value.charAt(0).toUpperCase() + value.slice(1);
}
emit('update:modelValue', value);
}
</script>
<template>
<input :value="modelValue" @input="updateValue" />
</template>
<!-- 父组件:直接享受自定义修饰符 -->
<my-input v-model.capitalize="text" />
💡 Vue 3 还支持多个 v-model:
v-model:title、v-model:content,大型表单组件特别实用。
9. 组件基础
9.1 一个标准组件的样子
<template>
<div class="component">
<h1>{{ title }}</h1>
<button @click="handleClick">通知父组件</button>
</div>
</template>
<script setup>
// Vue 3 推荐写法
const props = defineProps({
title: { type: String, required: true }
});
const emit = defineEmits(['click']);
function handleClick() {
emit('click', 'some data');
}
</script>
<style scoped>
.component { padding: 1rem; }
</style>
9.2 Props 校验:写出让同事感谢你的组件
props: {
propA: Number, // 类型检查
propB: [String, Number], // 多种类型
propC: { type: String, required: true },// 必填
propD: { type: Number, default: 100 }, // 默认值
propE: { // ⚠️ 对象/数组默认值必须用函数返回
type: Object,
default() { return { message: 'hello' }; }
},
propF: { // 自定义校验
validator(value) {
return ['success', 'warning', 'danger'].includes(value);
}
}
}
⚠️ 常见坑:
default: {}直接写对象会导致所有组件实例共享同一个引用,必须用函数返回。
9.3 单向数据流:子组件为什么不该改 props
props: ['initialCounter'],
data() {
// ✅ 方案一:用 prop 初始化本地 data(之后与父组件无关)
return { counter: this.initialCounter };
},
computed: {
// ✅ 方案二:用 computed 派生(随 prop 联动)
normalizedSize() {
return this.initialSize.trim().toLowerCase();
}
}
// ✅ 方案三(万能):$emit 通知父组件改(见下一节)
💬 面试题精选
Q1:Props 的单向数据流是什么?
答:Props 是单向下行绑定——父组件数据变化会流到子组件,但子组件不允许反向修改 props(Vue 会发出警告)。
需要"修改"的正确姿势:
- 用 prop 初始化本地
data(一次性拷贝) - 用
computed派生新值(保持联动) $emit事件通知父组件自己改(数据源始终归父组件管)
💡 加分话术:单向数据流的意义在于让数据流向可预测——修改只发生在一个地方(数据源),排查问题时不用在多个组件里找谁在改数据。这也是 Pinia/Vuex 存在的理论基础。
Q2:如何进行 Props 验证?
答:通过 type(类型)、required(必填)、default(默认值)、validator(自定义校验函数)四个维度。开发环境校验失败会在控制台给出明确警告,生产环境自动关闭以节省性能。
10. 组件通信
💡 面试时这题的标准答法是按场景分类,而不是罗列 API。下面直接给你一套完整话术。
10.1 通信方式全景表(必背)
| 方式 | 方向 | 适用场景 | 一句话点评 |
|---|---|---|---|
| Props | 父 → 子 | 父传数据给子 | 最基础,默认答案 |
| $emit | 子 → 父 | 子通知父(事件+数据) | 配合 props 构成单向数据流 |
| defineExpose + ref | 父 → 子 | 父调用子的方法/属性 | 少用,破坏封装,UI 库封装常用 |
| $parent / $root | 子 → 父/根 | 快速访问(不推荐) | 耦合严重,难维护 |
| Provide / Inject | 祖先 → 后代 | 跨多级(主题、用户信息) | 单向,后代不可回传 |
| Event Bus / mitt | 任意 | 跨组件(无父子关系) | 小型项目可用,大了易失控 |
| Pinia / Vuex | 任意 | 多组件共享状态 | 中大型项目标配 |
10.2 父子通信实战
<!-- 父组件 -->
<script setup>
import { ref } from 'vue';
import Child from './Child.vue';
const parentMsg = ref('Hello');
const childRef = ref(null);
function handleUpdate(msg) {
parentMsg.value = msg; // 收到子组件的更新
}
function callChild() {
childRef.value.someMethod(); // Vue3 需子组件 defineExpose 暴露
}
</script>
<template>
<Child :message="parentMsg" @update="handleUpdate" ref="childRef" />
</template>
<!-- 子组件 -->
<script setup>
const props = defineProps(['message']);
const emit = defineEmits(['update']);
function notify() {
emit('update', 'New Message');
}
function someMethod() { /* ... */ }
defineExpose({ someMethod }); // 暴露给父组件 ref 调用
</script>
10.3 Event Bus 的演进
// Vue 2:直接用一个新的 Vue 实例当事件中心
import Vue from 'vue';
export const EventBus = new Vue();
EventBus.$emit('message', 'Hello');
EventBus.$on('message', (msg) => console.log(msg));
EventBus.$off('message'); // ⚠️ 组件销毁前记得 off,否则重复监听
// Vue 3:移除了 $on/$off,官方推荐第三方库 mitt
import mitt from 'mitt';
const emitter = mitt();
emitter.on('message', (msg) => {});
emitter.emit('message', 'Hello');
emitter.off('message');
⚠️ Event Bus 最大的坑:监听不解除导致重复触发和内存泄漏。如果用了,务必在
onBeforeUnmount里off。
💬 面试题精选
Q1:Vue 组件通信有哪些方式?
答(按场景组织):
- 父子通信:Props 下发、
$emit上报、(Vue 3)defineExpose+ ref - 跨层级:Provide / Inject
- 任意组件:Event Bus(mitt)、Pinia/Vuex
- 边界情况:
$attrs透传未声明的属性和事件、v-model双向通信
💡 加分话术:实际项目里,Event Bus 我没有用过——跨组件通信优先用 Pinia 管理共享状态,数据流向清晰且可调试(Vue DevTools 能追踪 mutation),Event Bus 在大型项目里事件来源难以追踪,后期维护成本高。
Q2:$attrs 是什么?
答:$attrs 包含了父组件传递的、但未被 props 声明的所有属性(class、style 除外,它们会单独处理)。常用于"包装组件"把属性透传给内部真实组件:
<!-- Vue 2 中 $listeners 存放未处理的事件;Vue 3 已合并进 $attrs -->
<base-input v-bind="$attrs" @click="$emit('click', $event)" />
11. 生命周期
💡 生命周期不是背口诀,而是要知道每个阶段能拿到什么、该做什么。
11.1 Vue 2 生命周期全流程
new Vue()
│
├─ beforeCreate 实例初始化完成,data/computed 都还没有 ❌ 访问不到数据
│
├─ created ✅ data/computed/methods 就绪,DOM 还没生成
│ → 发 API 请求、初始化数据(最早能拿到数据的地方)
│
├─ beforeMount 模板编译完成,即将挂载
│
├─ mounted ✅ DOM 已渲染,可访问 $el
│ → 操作 DOM、启动定时器、初始化第三方库(图表/地图)
│
├─ beforeUpdate 数据变了,DOM 更新前
│
├─ updated DOM 已更新(⚠️ 避免在这里再改数据,会死循环)
│
├─ beforeDestroy 实例即将销毁
│ → 清理定时器、移除全局事件监听、取消未完成的请求
│
└─ destroyed 实例已销毁
11.2 Vue 3 生命周期对照
setup() ← 替代 beforeCreate + created(组件实例创建前执行)
│
├─ onBeforeMount / onMounted 挂载前后
├─ onBeforeUpdate / onUpdated 更新前后
├─ onBeforeUnmount / onUnmounted 卸载前后(对应 Vue2 的 beforeDestroy/destroyed)
│
└─ 特有钩子:
onActivated / onDeactivated ← keep-alive 缓存组件激活/停用
onErrorCaptured ← 捕获后代组件错误(可做全局错误上报)
11.3 实战中最常用的三个钩子
<script setup>
import { onMounted, onBeforeUnmount, onErrorCaptured } from 'vue';
let timer = null;
onMounted(() => {
// ① 发请求 / 操作 DOM / 启动定时器
timer = setInterval(() => { /* ... */ }, 1000);
});
onBeforeUnmount(() => {
// ② 清理工作:防内存泄漏的底线
clearInterval(timer);
window.removeEventListener('resize', handler);
});
// ③ 错误捕获(可选):统一上报
onErrorCaptured((err, instance, info) => {
console.error('组件错误:', err, info);
return false; // false 阻止错误继续向上传播
});
</script>
💬 面试题精选
Q1:created 和 mounted 的区别? ⭐必考
答:
created:实例创建完成,data/computed 已初始化,但 DOM 不存在。适合发起 API 请求、做数据初始化——能更早发请求,减少白屏时间。mounted:DOM 已渲染,可以访问 DOM 元素、测量尺寸、初始化依赖 DOM 的第三方库(ECharts、地图)。适合操作 DOM、启动定时器。
💡 加分话术:Vue 3 的
setup()合并了这两个阶段,所以请求一般直接写在setup顶部;需要 DOM 的逻辑才放进onMounted。
Q2:beforeDestroy / onBeforeUnmount 中应该做什么?
答:所有清理工作:
clearInterval/clearTimeout清除定时器removeEventListener移除在 window/document 上加的全局监听- 取消未完成的网络请求(防止回调里操作已销毁的实例)
- 销毁第三方库实例(echarts.dispose() 等)
💡 反面教材话术:不做清理会导致内存泄漏——组件没了但定时器还在跑,闭包引用着组件实例,整个组件无法被垃圾回收,页面越用越卡。
Q3:Vue 3 的 setup 替代了哪些生命周期?
答:setup 在组件实例创建之前执行,替代了 beforeCreate 和 created。在 setup 中访问不到 this(实例还没创建),所有逻辑通过显式导入的 API(ref、onMounted 等)完成——这也是 Composition API "更函数式、更好做类型推断"的原因之一。
Q4:父组件和子组件的生命周期执行顺序?(进阶追问)
答:
- 挂载阶段:父 beforeMount → 子 beforeCreate → 子 created → 子 mounted → 父 mounted(子先完成挂载,父才能确认整体挂载完成)
- 更新阶段:父 beforeUpdate → 子 beforeUpdate → 子 updated → 父 updated
- 销毁阶段:父 beforeDestroy → 子 beforeDestroy → 子 destroyed → 父 destroyed
💡 记忆口诀:父组件等子组件——挂载和更新都是子组件先完成,父组件最后收尾。
📋 面试知识点速查表
面试当天早上花 10 分钟扫一遍,快速唤醒记忆。
| 问题 | 一句话答案 |
|---|---|
| Vue2/Vue3 响应式区别 | defineProperty 逐个劫持、检测不到新增属性 vs Proxy 劫持整个对象 |
| ref vs reactive | 基本类型用 ref(.value),对象用 reactive(不可整体替换) |
| 解构丢响应性怎么办 | toRefs() |
| computed vs methods | computed 有缓存 |
| watch vs watchEffect | watch 显式指定源、能拿旧值;watchEffect 自动追踪、立即执行 |
| v-if vs v-show | 销毁重建 vs display 切换 |
| v-for 为什么要 key | 帮助 diff 识别节点身份;index 做 key 会状态错乱 |
| v-for 和 v-if 优先级 | Vue2: v-for 高;Vue3: v-if 高(访问不到循环变量) |
| v-model 原理 | :value + @input 的语法糖(Vue3: modelValue + update:modelValue) |
| props 为什么单向 | 数据流向可预测,方便排查问题 |
| 组件通信方式 | props/emit、provide/inject、Pinia、mitt |
| created vs mounted | 前者无 DOM 可发请求,后者有 DOM 可操作 |
| 卸载钩子做什么 | 清定时器、移除全局监听、取消请求 |
| setup 替代了什么 | beforeCreate + created |
📌 最后
- 如果觉得有用,欢迎 点赞 ❤️ + 收藏 ⭐,面试前翻出来看看
- 第 2 部分(Composition API 进阶 / 插槽 / 自定义指令 / Provide-Inject)正在整理中,关注不迷路
- 有遗漏的知识点或不同见解,欢迎在评论区补充,我会持续更新本文 🙏

2443

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



