
哈喽小伙伴们大家好,今天我们来看看Vue相关的面试题,整理不容易,看完如果觉得对你有帮助的话欢迎给个三连支持哦~
一、基础使用:
1、Vue 生命周期(8 个钩子函数 + 执行时机,如 created 与 mounted 区别)

看了上面的图片,相信你已经有个大概的了解了。
对于一个vue组件来说,它的生命周期无非就四个:创建,挂载,更新,销毁。
在创建阶段:主要涉及到两个钩子函数:beforeCreate()和created()。前者表示实例刚被创建,数据观测(data observer)和事件配置尚未完成,无法访问 data、methods 等。而后者则是实例创建完成,已完成数据观测和事件配置,可访问 data、methods,但 DOM 尚未生成($el 不存在)。
创建结束之后:组件会进入挂载阶段,在这个阶段主要有下面两个钩子会被执行:beforeMount()和mounted()。前者用于在模板编译 / 挂载前,
$el 已存在但未挂载到页面。
而mounted阶段则表示模板编译 / 挂载完成,DOM 已生成,可访问 DOM 元素(如通过 $refs),常用作初始化操作(如请求数据、初始化插件)。
我们的DOM元素挂载到页面上之后,接下来我们就要通过JS逻辑去控制视图的更新,因此,我们会进入更新阶段。
那在更新阶段呢,也有两个钩子函数会执行,beforeUpdate()和updated()。前者表示数据更新后,DOM 重新渲染前调用,可获取更新前的 DOM 状态。后者表示DOM 重新渲染完成后调用,可获取更新后的 DOM 状态(避免在此修改数据,可能导致无限循环)
当一个组件完成了它的使命后,为了防止内存被一直占用,以免造成一些其他的问题,我们就要及时将组件销毁。
同样,在vue中,也有专门的钩子去销毁组件。分别是beforeDestroy()和destroyed()
其中,beforeDestroy()钩子会在实例销毁前调用,仍可访问实例属性和方法,常用于清理操作(如清除定时器、解绑事件)。
而destroyed()则是在实例销毁后调用,所有事件监听器和子实例已被移除。
2、指令
vue中的指令其实分为两种,一种是vue本身内置的指令,另一种是开发者自己编写的自定义指令。
先来看看Vue中自内置的指令。
这些指令通常以v-为前缀,作用于HTML元素,用于在模板中实现各种功能。
常见的有以下几个:
1)、v-text
<p v-text="message"></p> <!-- 等价于 <p>{{ message }}</p> -->2)、v-html
<div v-html="rawHtml"></div> <!-- 渲染 HTML 字符串 -->3)、v-bind
<img v-bind:src="imageUrl">
<a :href="linkUrl">链接</a> <!-- 缩写形式 -->
<div :class="{ active: isActive }"></div> <!-- 绑定类名 -->
<div :style="{ color: textColor, fontSize: '16px' }"></div> <!-- 绑定样式 -->上面三个是用于绑定数据的。
4)、v-on:用于绑定事件监听器
<button v-on:click="handleClick">点击</button>
<button @click="handleClick">点击</button> <!-- 缩写形式 -->
<button @click="count++">计数器</button> <!-- 直接执行表达式 -->
<form @submit.prevent="handleSubmit">提交</form> <!-- 带修饰符 -->常见的修饰符有四个:
.prevent:阻止默认行为(如表单提交)
.stop:阻止事件冒泡
.once:事件只触发一次
.enter:仅响应回车键(键盘事件)
5)、v-if
<p v-if="isShow">显示内容</p>
<p v-else-if="isLoading">加载中...</p>
<p v-else>隐藏内容</p>6)、v-show
<p v-show="isVisible">始终存在于 DOM 中</p>这两个主要用于条件渲染。
它俩的区别在于v-if是通过条件判断来决定销毁或者重建这个元素。而v-show则是通过控制元素的display属性来决定是否展示这个元素。
前者适合条件切换不频繁的场景,而 v-show 更适合频繁切换的场景。
3、组件通信
vue中组件通信的方式其实有很多种。
我们先来看父子组件之间的通信方式:
1)、父传子:父组件通过v-bind绑定数据,子组件通过props接收数据。
<!-- 父组件 -->
<template>
<ChildComponent :message="parentMsg" :user="userInfo" />
</template>
<script>
export default {
data() {
return {
parentMsg: "来自父组件的消息",
userInfo: { name: "张三", age: 20 }
};
}
};
</script>
<!-- 子组件 ChildComponent -->
<template>
<div>
<p>{{ message }}</p>
<p>{{ user.name }}</p>
</div>
</template>
<script>
export default {
// 声明接收的 props(可指定类型、默认值、验证等)
props: {
message: String,
user: {
type: Object,
required: true
}
}
};
</script>2)、子传父
this.$emit('事件名', 数据) 触发自定义事件。v-on 监听事件并接收数据。<!-- 子组件 ChildComponent -->
<template>
<button @click="sendData">向父组件发送数据</button>
</template>
<script>
export default {
methods: {
sendData() {
// 触发自定义事件,并传递数据
this.$emit("child-event", "来自子组件的数据", 123);
}
}
};
</script>
<!-- 父组件 -->
<template>
<ChildComponent @child-event="handleChildData" />
</template>
<script>
export default {
methods: {
handleChildData(data1, data2) {
console.log(data1, data2); // 输出:"来自子组件的数据" 123
}
}
};
</script>3)、父子组件双向绑定数据:通过v-model和sync来绑定和修改数据。
v-model:本质是 value 属性 + input 事件的语法糖,适用于表单类组件。
.sync:简化子组件修改父组件数据的写法(Vue 3 推荐用 v-model 替代)。
废话不多说,3,2,1,上代码!
<!-- 子组件 -->
<template>
<input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" />
</template>
<script>
export default {
props: ["modelValue"] // v-model 默认绑定的 prop 名
};
</script>
<!-- 父组件 -->
<template>
<ChildComponent v-model="username" />
</template>
<script>
export default {
data() { return { username: "" }; }
};
</script>紧接着,我们来看一看兄弟组件之间的通信:
有两种方式:一种是通过父组件来共享数据,另外一种是通过事件总线来管理。
我们来看看通过事件总线是怎么来共享数据的。
// eventBus.js
import mitt from 'mitt';
export const bus = mitt();
// 组件 A(发布事件)
import { bus } from './eventBus';
bus.emit('share-data', '兄弟组件的数据');
// 组件 B(订阅事件)
import { bus } from './eventBus';
bus.on('share-data', (data) => {
console.log(data); // 输出:"兄弟组件的数据"
});那除了上面的两种方式之外,如果两个组件不是父子关系呢?那我们怎么通信。
vue也提供了顶层组件和底层组件之间的通信方式。
其中最常用的就是provide/inject。父组件通过 provide 提供数据,任意深层子组件通过 inject 获取数据(Vue 2 和 Vue 3 均支持),看了下面的代码你就知道了。
// 父组件(顶层)
export default {
provide() {
return {
theme: "dark",
changeTheme: (value) => { this.theme = value; } // 可传递方法
};
}
};
// 深层子组件
export default {
inject: ["theme", "changeTheme"], // 注入数据
mounted() {
console.log(this.theme); // 输出:"dark"
this.changeTheme("light"); // 调用父组件方法
}
};其实除了provide/inject之外,状态管理不也是一种跨层级组件之间通信的方式么?
当然,这种方式其实更加适合一些大型的项目,统一管理全局状态,任意组件可直接读写共享数据。同时,也会涉及到一些其他的依赖,看看是怎么实现的吧~
// store/counter.js(定义仓库)
import { defineStore } from 'pinia';
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
actions: {
increment() { this.count++; }
}
});
// 组件 A(修改状态)
import { useCounterStore } from './store/counter';
const store = useCounterStore();
store.increment();
// 组件 B(读取状态)
import { useCounterStore } from './store/counter';
const store = useCounterStore();
console.log(store.count); // 输出:1二、核心原理:
1、数据响应式(Vue2 Object.defineProperty 缺陷、Vue3 Proxy 优势)
因为vue它是一个数据和视图双向绑定的框架。每当服务器返回的数据变化的时候视图也会对应的更新,那vue是怎么知道这些数据变化了呢?今天我们一起来看一看。
数据响应式的核心其实是追踪数据变化并触发视图更新:
对于这一点,vue2和vue3的实现方式是不一样的。
Vue 2 通过 Object.defineProperty 对数据对象的属性进行劫持,来实现响应式。
看下面的代码:
function defineReactive(obj, key, value) {
// 递归处理嵌套对象
observe(value);
Object.defineProperty(obj, key, {
get() {
// 依赖收集:记录当前使用该属性的组件
console.log(`读取了 ${key} 的值:${value}`);
return value;
},
set(newValue) {
if (newValue !== value) {
// 递归处理新值(如果是对象)
observe(newValue);
value = newValue;
// 触发更新:通知依赖该属性的组件重新渲染
console.log(`修改了 ${key} 的值:${newValue}`);
}
}
});
}
// 对对象的所有属性进行响应式处理
function observe(obj) {
if (typeof obj !== 'object' || obj === null) return;
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key]);
});
}
// 测试
const data = { name: 'Vue 2', age: 5 };
observe(data);
data.name; // 触发 get:"读取了 name 的值:Vue 2"
data.name = 'Vue 2.0'; // 触发 set:"修改了 name 的值:Vue 2.0"当时这种方式其实很多缺点。其一,Object.defineProperty 只能劫持已存在的属性,新增属性或删除已有属性时,无法触发更新。举个例子:
data.sex = 'male'; // 新增属性,不会触发响应式
delete data.age; // 删除属性,不会触发响应式其二,但数组发生变化时,它也没有办法监听
对于数组,Object.defineProperty 只能劫持 length 和索引属性,但 Vue 2 没有对索引进行劫持(性能代价高),因此:
arr[0] = 1)不会触发更新。arr.length = 0)不会触发更新。需通过数组变异方法(push/pop/splice 等,Vue 2 对这些方法进行了重写)才能触发响应式。还有一个,就是涉及到性能的问题。当它对嵌套对象进行响应式处理时,需要递归遍历所有属性,深层嵌套对象会增加初始化成本。
那在vue2的基础上,vue3关于响应式的实现也做了很多改进。我们一起来看看vue3是怎么实现响应式的?
Vue 3 改用 Proxy 对整个对象进行代理,从根本上解决了 Vue 2 的缺陷。
我们来看详细的代码:
function reactive(obj) {
if (typeof obj !== 'object' || obj === null) return obj;
return new Proxy(obj, {
get(target, key, receiver) {
const value = Reflect.get(target, key, receiver);
track(target, key); // 依赖收集
// 对嵌套对象进行惰性代理
return reactive(value);
},
set(target, key, newValue, receiver) {
const oldValue = Reflect.get(target, key, receiver);
if (oldValue !== newValue) {
Reflect.set(target, key, newValue, receiver);
trigger(target, key); // 触发更新
}
},
deleteProperty(target, key) {
const hadKey = Reflect.has(target, key);
const result = Reflect.deleteProperty(target, key);
if (hadKey && result) {
trigger(target, key); // 删除属性时触发更新
}
return result;
}
});
}总结一下:
1)、proxy代理的是整个对象,属性的增加和删除都能被监听到
2)、Proxy 对嵌套对象采用 “惰性代理”:只有当访问嵌套对象时,才会为其创建代理,避免了 Vue2 初始化时的深度遍历,对复杂对象更友好。
3)、Proxy 可以代理 ES6 新增的 Map、Set 等集合类型,而 Object.defineProperty 只能处理普通对象。
对比一下:
特性 | Vue2(Object.defineProperty) | Vue3(Proxy) |
|---|---|---|
监听范围 | 仅已定义属性 | 整个对象(包括新增 / 删除属性) |
数组支持 | 需重写原型方法,有局限 | 原生支持所有数组操作 |
嵌套对象处理 | 初始化时深度遍历 | 惰性代理(访问时才代理) |
复杂数据结构 | 不支持 Map、Set 等 | 支持 Map、Set 等 |
性能 | 初始化成本高(深层对象) | 初始化成本低,按需代理 |
可以看出来,vue3确实是做出了很大的改进。那你想一想:react是怎么处理的呢?为什么要这么处理?答案下期揭晓~
2、虚拟 DOM(原理 + diff 算法核心)
这里补充一些问题:
1)、什么是DOM?
2)、所谓的视图更新,归根结底更新的是啥?
3)、浏览器是怎么知道视图该更新了?
如果你的js基础比较扎实的话,上面三个问题你应该很清楚。
虚拟 DOM 它其实是对真实 DOM 的抽象表示,本质上是一个轻量级的 JavaScript 对象,用于描述 DOM 节点的结构和属性。
它的核心原理无非就这四个:
我们来看一个例子:
// 真实DOM
<div class="container">
<h1>Hello World</h1>
<p>Virtual DOM Demo</p>
</div>
// 对应的虚拟DOM表示
{
tag: 'div',
props: { class: 'container' },
children: [
{ tag: 'h1', props: {}, children: ['Hello World'] },
{ tag: 'p', props: {}, children: ['Virtual DOM Demo'] }
]
}上面的这些代码眼熟不?你知道它在浏览器的哪里吗?
3、模板编译过程
在回答这个问题之前,先来看一看什么是模板编译:
所谓模板编译,它就是将模板文件(包含静态内容和动态语法标记)转换为可执行代码(或中间代码),最终生成动态内容(如网页、文档、邮件等)的过程。其核心目标是将 “静态结构 + 动态逻辑” 分离的模板,高效转化为计算机可执行的指令,同时兼顾性能、安全性和可维护性。
模板编译通常分为 3 个核心阶段:解析(Parsing)、优化(Optimization)、生成(Code Generation)。
解析是编译的 “读入” 环节,目标是将人类可读的模板文本,转化为计算机可理解的抽象语法树(Abstract Syntax Tree,AST)(一种描述代码结构的树形数据结构)。该阶段又分为两步:词法分析和语法分析。
词法分析器(Tokenizer/Lexer)会逐行扫描模板文本,将其拆分为一系列不可再分的最小语法单元(Token)。不同模板引擎的 Token 类型不同,常见类型如下:
<div class="box">、Hello。{{ username }}(Vue/Jinja2)、<%= name %>(EJS)。
逻辑标记:{% if isLogin %}(Jinja2)、v-if="show"(Vue 指令)。
循环标记:{% for item in list %}(Jinja2)、v-for="item in list"(Vue)。语法分析器(Parser)会根据模板引擎的语法规则(如 “{{ 必须与 }} 配对”“if 必须有对应的 endif”),对 Token 序列进行校验和结构化,最终生成 AST。
AST 的每个节点对应模板的一个语法单元(如静态文本节点、变量节点、条件节点、循环节点),节点间通过父子关系体现模板的嵌套结构。
优化阶段的目标是对 AST 进行改造,减少冗余计算、规避安全风险,提升最终代码的执行效率和安全性。该阶段在前端模板(如 Vue、React)中尤为重要,后端模板(如 Jinja2)优化相对简化。
常见的优化手段包括:
例:<div class="header">Title</div> 是静态节点,编译后会被缓存,数据更新时不重新处理。
例:模板中的 {{ 1 + 2 * 3 }} 会被预计算为 {{ 7 }},运行时直接输出结果。
{{ username }})进行 HTML 转义(如将<转义为<),防止 XSS 攻击(多数模板引擎默认开启,如 Jinja2、Vue)。
{% if false %}...{% endif %}中的内容)。
生成阶段是编译的 “输出” 环节,目标是将优化后的 AST 转化为目标平台可执行的代码(如 JavaScript、Python、Java 字节码等),最终通过执行该代码生成动态内容。
不同模板引擎的 “目标代码” 类型不同,取决于其运行环境:
4、vue中的状态管理:
Vue的状态管理主要有两种方案:一种是Vuex,另一种是Pinia
不管使用 Vuex 还是 Pinia,都围绕以下核心概念设计:
userInfo、cartList)。
Vuex和Pinia两者的核心思想是一致的,但 Pinia 语法更简洁,且原生支持 Vue 3 的 Composition API。
接下来我们以Pinia为例,详细介绍一下它是怎么管理状态的。
首先,安装Pinia:
npm install pinia紧接着,创建Pinia实例并注入应用
// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
const pinia = createPinia() // 创建 Pinia 实例
app.use(pinia) // 注入应用
app.mount('#app')第三步:定义store
Store 是保存状态和修改方法的容器,每个 Store 对应一个功能模块(如用户、购物车)。
// stores/cart.js(使用 Composition API 风格)
import { defineStore } from 'pinia'
// 定义并导出 Store,id 需唯一(用于 devtools 识别)
export const useCartStore = defineStore('cart', {
// 状态:返回初始数据的函数
state: () => ({
list: [], // 购物车商品列表
count: 0 // 商品总数(也可通过 getter 计算,此处为示例)
}),
// 派生状态:类似组件的 computed,依赖 state 生成新数据
getters: {
totalPrice: (state) => {
// 计算购物车总价
return state.list.reduce((sum, item) => sum + item.price * item.quantity, 0)
}
},
// 方法:包含同步和异步操作,用于修改 state
actions: {
// 同步操作:直接修改 state
addItem(item) {
const existing = state.list.find(i => i.id === item.id)
if (existing) {
existing.quantity += 1
} else {
state.list.push({ ...item, quantity: 1 })
}
this.count += 1 // 可通过 this 访问 state 中的属性
},
// 异步操作:如从接口获取购物车数据
async fetchCart() {
const res = await fetch('/api/cart')
const data = await res.json()
this.list = data.list // 直接修改 state
this.count = data.count
}
}
})最后,在组件中使用store
<!-- CartComponent.vue -->
<template>
<div>
<p>商品总数:{{ cartStore.count }}</p>
<p>总价:{{ cartStore.totalPrice }}</p>
<button @click="handleAdd">添加商品</button>
</div>
</template>
<script setup>
import { useCartStore } from '@/stores/cart'
// 获取 Store 实例
const cartStore = useCartStore()
// 调用 actions 中的方法
const handleAdd = () => {
cartStore.addItem({ id: 1, name: '商品A', price: 99 })
}
// 组件挂载时加载购物车数据
cartStore.fetchCart()
</script>Pinia的核心特性主要有以下几个:
1)、简化的 API:无需区分 mutations(同步)和 actions(异步),统一用 actions 处理所有操作。
2)、自动响应式:Store 中的 state 会自动转为响应式数据,组件使用时无需额外处理。
3)、模块化设计:每个 Store 就是一个独立模块,无需像 Vuex 那样嵌套 modules。
4)、Composition API 支持:可直接在 setup 中使用,也支持 Options API(如 mapState)。
5)、开发工具集成:Pinia 与 Vue Devtools 深度集成,可追踪状态变化历史。
以上就是状态管理的内容。
三、Vue3 新特性:
1、Composition API(setup 函数、ref/reactive)
Vue 3 引入的 Composition API 是一套全新的逻辑组织和复用方案,相比 Vue 2 的 Options API(通过 data、methods、computed 等选项组织代码),它更灵活,尤其适合处理复杂组件的逻辑。核心特性包括 setup 函数、ref/reactive 响应式 API 以及生命周期钩子的调整。
setup 是组件初始化前执行的函数,是 Composition API 的核心入口,所有组合式 API 都在此函数内使用。
beforeCreate 和 created 之间)。this 指向 undefined(不能通过 this 访问组件选项)。setup 接收两个参数:
props:组件接收的 props 数据(响应式,会随父组件传递的值更新)。context:上下文对象,包含组件的核心属性和方法,如:attrs:非 props 的属性集合(类似 this.$attrs)。
slots:插槽内容(类似 this.$slots)。
emit:触发自定义事件的方法(类似 this.$emit)。
看下面的代码:
<template>
<div>{{ message }}</div>
</template>
<script>
export default {
props: {
title: String
},
setup(props, context) {
// 访问 props(响应式)
console.log(props.title)
// 触发事件
context.emit('custom-event', 'data')
// 定义响应式数据
const message = 'Hello Composition API'
// 返回值会暴露给模板和组件的其他选项
return { message }
}
}
</script>在setup中返回的值(一般是对象)会直接暴露给模板,在模板中就可以直接使用这些对象的属性和方法。
在vue中,有两个响应式的API,分别是ref 和 reactive,它俩具体有什么区别,接下来我们一探究竟。
ref 用于将基本类型(如字符串、数字、布尔值)或引用类型(对象、数组)转为响应式数据。
value 属性的对象。.value)。.value 操作。看下面的这段代码:
<template>
<div>
<p>计数:{{ count }}</p>
<button @click="increment">+1</button>
</div>
</template>
<script>
import { ref } from 'vue'
export default {
setup() {
// 创建响应式数据(基本类型)
const count = ref(0)
// 方法(修改时需用 .value)
const increment = () => {
count.value++
}
return { count, increment }
}
}
</script>如果用 ref 包装对象 / 数组,内部会自动通过 reactive 转为响应式代理,脚本中修改时仍需用 .value
reactive 用于将对象或数组转为响应式数据(基于 ES6 Proxy 实现)。
.value,直接操作属性。举个例子:
<template>
<div>
<p>姓名:{{ user.name }}</p>
<p>年龄:{{ user.age }}</p>
<button @click="user.age++">增长年龄</button>
</div>
</template>
<script>
import { reactive } from 'vue'
export default {
setup() {
// 创建响应式对象
const user = reactive({
name: 'Alice',
age: 20
})
return { user }
}
}
</script>reactive 有 “深层响应式” 特性,修改嵌套属性也会触发更新
同样都是响应式的API,我们怎么决定使用ref还是reactive呢?
对于基本类型,必须要使用ref
对于对象或者数组,两者都是可以的。
但是如果是简单对象的话,推荐使用reactive(语法更简洁)。
如果是可能被重新赋值的对象,此时推荐使用ref(reactive 无法直接替换整个对象)。
如果用错了,有些情况下响应式会失效,比如说下面这种情况。
// reactive 无法直接替换对象(会丢失响应式)
const user = reactive({ name: 'Alice' })
user = { name: 'Bob' } // 错误:响应式失效
// ref 可直接替换对象(保持响应式)
const user = ref({ name: 'Alice' })
user.value = { name: 'Bob' } // 正确四、扩展:
1、对比 Vue2 与 Vue3 核心差异
最后,我们一起来看看Vue2 与 Vue3 有哪些区别?这个也是面试中非常容易被问到的一个点,包括我面试别人的时候也要去问这个问题。
首先就是关于响应式的实现原理,上面也介绍了,这里给大家看一张表格。
特性 | Vue2 | Vue3 |
|---|---|---|
实现原理 | Object.defineProperty | Proxy + Reflect |
数据类型支持 | 仅支持对象 / 数组 | 支持所有数据类型(对象、数组、基本类型等) |
嵌套响应式 | 需递归遍历对象手动实现 | 天然支持深层响应式(Proxy 代理整个对象) |
新增属性响应 | 需用 Vue.set(obj, key, val) | 直接赋值即可(obj.newKey = val) |
数组索引修改 | 不响应(需用 Vue.set 或变异方法) | 直接响应(arr[0] = val 有效) |
响应式触发时机 | 依赖属性 getter/setter 拦截 | 依赖 Proxy 陷阱(更全面的拦截能力) |
其次就是API的风格:在vue2中使用选项式API,通过 data、methods、computed 等选项组织代码,逻辑分散在不同选项中。在vue3中使用组合式API,按逻辑相关性组织代码,支持逻辑复用。
第三,生命周期函数。
在Vue2中,以选项形式声明(如 mounted() {})。而在Vue3中,Composition API 中以函数形式导入使用(如 onMounted(() => {})),且部分钩子更名(如 beforeDestroy → onBeforeUnmount)。
除此之外,Vue3也提供了更多实用的模板特性。
例如v-model和setup语法糖。
第五就是状态管理。
Vue2:官方推荐 Vuex,需区分 mutations(同步)和 actions(异步),模块化需通过 modules 嵌套。
Vue3:官方推荐 Pinia(Vuex 的继任者),语法更简洁:
mutations 和 actions,统一用 actions 处理所有操作。第六,性能优化:
第七,typescript的支持。
vue-class-component 等库增强,类型推断不友好。
好啦,本期文章就到这里,如果大家觉得有需要补充的也可以在评论区提出来,我们下期再见~