Vue 面试基础篇:Vue 核心知识点保姆级整理,11 个章节 + 高频面试题全解析

🔥前端面试必看:Vue 核心知识点保姆级整理,11 个章节 + 高频面试题全解析

从模板语法到响应式原理,从组件通信到生命周期——这一篇就够了。


为什么写这篇文章

最近准备面试,把 Vue 从基础到进阶的知识点重新梳理了一遍。翻遍网上的文章,要么太浅只讲 API,要么太深直接啃源码——对面试来说都不合适。

所以我把面试官爱问的点、容易踩的坑、以及高频面试题的标准答法全部整理成了这份笔记,每一节都附有代码示例和面试话术。

适合谁看?

  • ✅ 正在准备前端面试的同学(每节末尾都有高频面试题精选)
  • ✅ 学过 Vue 但知识点零散,想系统梳理一遍的人
  • ✅ 正在从 Vue2 迁移到 Vue3 的开发者

阅读建议:全文约 1.5 万字,代码片段均可直接运行,建议收藏后配合自己的项目慢慢消化。文末附「知识点速查表」,面试前 10 分钟可速览一遍。


📑 目录

  1. Vue 概述
  2. 模板语法
  3. 数据绑定与响应式
  4. 计算属性与侦听器
  5. Class 与 Style 绑定
  6. 条件渲染与列表渲染
  7. 事件处理
  8. 表单处理
  9. 组件基础
  10. 组件通信
  11. 生命周期

📌 附:面试知识点速查表 —— 建议面试当天早上扫一遍


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 2Vue 3
响应式原理Object.definePropertyProxy
API 风格Options APIOptions API + Composition API
生命周期beforeCreate / createdsetup()
根节点单根节点支持多根节点(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 的核心特性有哪些?

答:

  1. 响应式数据绑定:数据变化自动更新视图
  2. 组件系统:UI 拆分为独立可复用的组件
  3. 虚拟 DOM:通过 diff 算法高效更新 DOM
  4. 指令系统v-ifv-forv-bind 等声明式指令简化开发
  5. 模板语法:基于 HTML 的模板语法,上手门槛低
  6. 渐进式框架:可以逐步引入,从简单的增强到完整的 SPA

💡 加分话术:如果面试官追问"为什么说 Vue 是渐进式的?",可以答:Vue 被设计为可以自底向上逐层应用——核心库只关注视图层,配合官方生态(Vue Router / Pinia / Vite)可以扩展成大型应用,甚至通过 Nuxt 做 SSR,各层可独立选用。

Q2:Vue 2 和 Vue 3 的主要区别?

答:

  1. 响应式:Vue 2 用 Object.defineProperty,Vue 3 用 Proxy(能检测新增/删除属性、数组索引)
  2. API:Vue 3 新增 Composition API,按逻辑关注点组织代码,更适合复用和大型项目
  3. 性能:Vue 3 有编译时优化(静态提升、Patch Flag)和 Tree-shaking 支持
  4. TypeScript:Vue 3 用 TypeScript 重写,类型推断更好
  5. Fragment:Vue 3 支持多根节点组件
  6. 新组件: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:到底用哪个?

对比项refreactive
支持类型任意(基本类型靠它)仅对象/数组
访问方式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 需要 .valuereactive 不需要?

答: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

特性watchwatchEffect
侦听源需要显式指定自动追踪(用到即追踪)
旧值✅ 能拿到 oldValue❌ 拿不到
执行时机默认懒执行(变化才跑)定义时立即执行一次
适用场景需要新旧值对比、精准控制副作用函数(发请求、同步 localStorage)

💬 面试题精选

Q1:计算属性和方法的区别?

答:核心区别是缓存。计算属性基于响应式依赖缓存结果,依赖不变时多次访问直接返回缓存;方法每次调用都重新执行。

💡 实战话术:模板中做格式化展示优先用 computed;事件回调、需要传参的逻辑用 methods。滥用方法做模板渲染在大列表场景是常见的性能瓶颈。

Q2:watchwatchEffect 的区别?

答:

  • 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-ifv-show 的区别? ⭐高频

答:

特性v-ifv-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-forv-if 为什么不建议一起使用?

答:

  • Vue 2v-for 优先级高于 v-if,每次循环都执行判断,性能浪费
  • Vue 3v-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-modelv-model:titlev-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 会发出警告)。

需要"修改"的正确姿势:

  1. 用 prop 初始化本地 data(一次性拷贝)
  2. computed 派生新值(保持联动)
  3. $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 最大的坑:监听不解除导致重复触发内存泄漏。如果用了,务必在 onBeforeUnmountoff

💬 面试题精选

Q1:Vue 组件通信有哪些方式?

答(按场景组织):

  1. 父子通信:Props 下发、$emit 上报、(Vue 3)defineExpose + ref
  2. 跨层级:Provide / Inject
  3. 任意组件:Event Bus(mitt)、Pinia/Vuex
  4. 边界情况$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:createdmounted 的区别? ⭐必考

答:

  • created:实例创建完成,data/computed 已初始化,但 DOM 不存在。适合发起 API 请求、做数据初始化——能更早发请求,减少白屏时间。
  • mountedDOM 已渲染,可以访问 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 在组件实例创建之前执行,替代了 beforeCreatecreated。在 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 methodscomputed 有缓存
watch vs watchEffectwatch 显式指定源、能拿旧值;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)正在整理中,关注不迷路
  • 有遗漏的知识点或不同见解,欢迎在评论区补充,我会持续更新本文 🙏
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

尾善爱看海

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值