Vue 必备知识点
本文整理了 Vue 2 时代常见面试知识点。Vue 2 的 Options API、Vue Router 3、Vuex 和
beforeDestroy/destroyed等内容保留并标注历史版本;Vue 3.5 的 Composition API、Router 4、Pinia 和新的生命周期写法在对应章节补充。
Vue 基础
Computed 和 Watch 的区别
- computed 计算属性:根据响应式依赖派生值,默认具有缓存。只有依赖变化并且重新读取时才会重新计算;getter 应保持纯净,不应在其中发请求或修改其他状态。
- watch 侦听器:显式监听一个或多个响应式来源,在来源变化后执行副作用,例如异步请求、日志、同步浏览器 API。它默认是惰性的,不是“任何数据变化都触发”。
- watchEffect:自动追踪回调同步执行阶段读取的依赖并立即执行,适合依赖列表不需要单独声明的场景。
import { computed, ref, watch } from 'vue'
const price = ref(10)
const count = ref(2)
const total = computed(() => price.value * count.value)
watch(total, (newTotal, oldTotal) => {
console.log('total changed:', oldTotal, '=>', newTotal)
})
当需要派生数据并在模板中重复使用时优先考虑 computed;当需要在变化后执行异步或有副作用的工作时使用 watch。深度监听和 flush 时机应按需要配置,不能把所有 watch 都写成 { deep: true }。
常见的事件修饰符及其作用
.stop:调用event.stopPropagation(),阻止事件继续传播。.prevent:调用event.preventDefault();只对可取消的默认行为有效,不等同于阻止冒泡。.capture:使用捕获阶段监听,由外到内执行。.self:只有事件目标正好是当前元素时才触发,子元素冒泡上来的事件不会满足条件。.once:事件监听器只执行一次。
v-if 和 v-show 的区别
v-if是真正的条件渲染,切换时会创建/卸载条件块中的 DOM、事件监听器和子组件;初始条件为假时具有惰性。v-show始终渲染元素,通过 CSS 的display控制显示和隐藏;切换开销小,但初始渲染和节点常驻开销更高。v-if适合条件很少改变或不需要初始渲染的内容;v-show适合频繁切换且内容较稳定的区域。
data 为什么是一个函数而不是对象?
在组件选项中,data 使用函数是为了让每个组件实例获得一份独立的数据对象:
export default {
data() {
return {
count: 0
}
}
}
如果多个组件实例共享同一个对象,某个实例修改数据会影响其他实例。data() 每次创建实例时返回新对象,可以避免这种共享引用问题。Vue 3 的 Options API 仍然遵循这一规则;Composition API 通常在 setup() 或 <script setup> 中声明状态。
Vue 中封装的数组方法有哪些?
Vue 2 会包装以下七个数组变异方法,使它们可以通知响应式依赖:
push、pop、shift、unshift、splice、sort、reverse
这不是因为 JavaScript 数组本身不能修改,而是因为 Vue 2 主要使用 Object.defineProperty() 观察已有属性,无法直接拦截数组索引和 length 的赋值。Vue 2 需要通过重写这些方法补上通知逻辑:
// Vue 2 中推荐
this.items.splice(index, 1, value)
this.$set(this.items, index, value)
Vue 3 使用 Proxy,可以直接追踪数组索引和 length;但仍建议使用清晰的数组操作,不要把“能拦截”误解成“没有任何性能成本”。
谈谈 MVVM
MVVM 通常分为 Model、View 和 ViewModel:
- Model 代表数据和业务状态。
- View 负责 UI 展示和用户交互。
- ViewModel 连接 Model 与 View,负责响应式更新和事件处理。
Vue 并不是严格意义上把所有代码都分成三个独立层的 MVVM 实现,但它通过响应式系统、模板和组件把状态变化映射到视图,减少了手动 DOM 操作。v-model 等语法还会把表单输入事件和状态更新组合起来,形成双向绑定体验。
使用 Object.defineProperty() 进行数据劫持的缺点
Vue 2 的响应式系统会递归遍历初始化时已有的对象属性,并用 getter/setter 收集依赖和派发更新。它的主要边界包括:
- 初始化后直接新增或删除对象属性,无法自动建立响应式。
- 直接通过数组索引赋值和直接修改
length无法被拦截。 - 需要递归转换已有嵌套结构,初始化和更新存在额外成本。
Vue 2 用 Vue.set、Vue.delete、splice 和数组方法包装解决部分限制。Vue 3 使用 Proxy 代理对象级别的操作,可以覆盖新增、删除、索引和集合操作;但 Proxy 也有运行环境兼容性、原始对象绕过代理、解构断开连接等边界,不能简单说成“完美监听任何变化”。
生命周期
Vue 2 历史生命周期
Vue 2 的生命周期顺序可以概括为:创建、挂载、更新、销毁:
beforeCreate:Vue 2 的内部事件系统已经初始化,但数据观测、computed、watch 和 methods 代理尚未完成,通常不能访问已代理的 data、computed 和 methods。created:实例选项已经处理,但 DOM 尚未挂载,不能依赖真实$el。beforeMount:首次渲染即将开始,尚未完成 DOM 挂载。mounted:实例 DOM 已挂载;不代表所有异步组件或异步数据都已经完成。beforeUpdate:响应式数据已经变化,但对应 DOM 尚未完成更新。updated:一次组件更新的 DOM patch 完成后调用,应避免在其中无条件修改状态,以免循环更新。beforeDestroy:销毁前调用,实例仍然可用。destroyed:销毁完成,Vue 2 的实例监听和子实例等已清理。activated/deactivated:被<keep-alive>缓存的组件进入或离开激活状态。
Vue 2 的简化父子顺序通常是:
父 beforeCreate -> 父 created -> 父 beforeMount
-> 子 beforeCreate -> 子 created -> 子 beforeMount
-> 子 mounted -> 父 mounted
更新时通常是父 beforeUpdate、子 beforeUpdate、子 updated、父 updated;销毁时是父 beforeDestroy、子 beforeDestroy、子 destroyed、父 destroyed。具体还会受到组件树、异步组件和插槽的影响,不能把这份顺序当成所有边界场景的完整规范。
Vue 3 当前生命周期
Vue 3 将销毁阶段改名为卸载阶段:
beforeDestroy→beforeUnmountdestroyed→unmounted- Composition API 对应
onBeforeUnmount()、onUnmounted()。
<script setup>
import { onMounted, onUnmounted } from 'vue'
function handleResize() {
// ...
}
onMounted(() => window.addEventListener('resize', handleResize))
onUnmounted(() => window.removeEventListener('resize', handleResize))
</script>
onMounted、onUnmounted 等 DOM 生命周期不会在服务端渲染期间执行。需要服务端预取数据时,应结合 onServerPrefetch() 或使用 Nuxt 等框架提供的数据获取方案;不能把 Vue 2 中“在 created 请求数据”当作 Vue 3 SSR 的通用答案。
一般在哪个生命周期请求异步数据?
Vue 2 客户端项目常在 created 或 mounted 中请求数据,取决于是否需要 DOM。Vue 3 客户端可以在 setup() 中创建同步 watcher、在 onMounted() 中访问浏览器 API,或使用框架的 SSR 数据预取。异步请求还应考虑竞态、取消和错误状态:
import { onMounted, onUnmounted, ref } from 'vue'
const data = ref(null)
const controller = new AbortController()
onMounted(async () => {
const response = await fetch('/api/data', {
signal: controller.signal
})
data.value = await response.json()
})
onUnmounted(() => controller.abort())
Vue 组件通信方式
父子组件
props:父组件向子组件传递数据。emits/$emit:子组件向父组件发出事件。v-model:约定值和更新事件的双向绑定语法。ref:父组件可以调用子组件公开的实例能力,但不应把它作为普通数据通信的首选。provide/inject:跨越多层祖先后代组件传递依赖。
Vue 3 中如果希望 provide 的值保持响应式,应提供 ref、reactive 或 computed;直接 provide 一个当时读取到的普通值不会自动形成响应式连接。更安全的模式是由 provider 提供状态和修改函数,并对只读状态使用 readonly()。
兄弟组件和任意组件
Vue 2 时代常见的 EventBus 是创建一个空 Vue 实例并使用 $on/$emit/$off。Vue 3 已移除实例事件 API $on、$off、$once,不能继续把 Vue 实例当事件总线。现在优先使用 props/emits、provide/inject、Pinia;确实需要独立事件总线时可使用 mitt 等库,并在组件卸载时移除监听。
Vue 2 的 $parent、$children、$refs 可以作为历史项目的逃生舱,但它们耦合组件层级,不适合替代清晰的 props/emits 契约。
Vue 进阶
Vue 路由模式有哪些?有什么区别?
Vue Router 常见的 URL 模式是 hash 和 HTML5 history:
- hash 路由把路径放在
#后面,hash 变化不会作为 HTTP 请求路径发送给服务器。 - history 路由使用
pushState、replaceState和popstate,地址更自然,但服务器必须把前端路由回退到应用入口,否则刷新深层路径可能返回 404。 - Vue Router 4 的
createMemoryHistory()适合测试、SSR 或没有浏览器地址栏的环境;Vue Router 3 的对应历史模式名称是mode: 'abstract'。
Vue Router 3 历史写法:
const router = new VueRouter({
mode: 'history', // 也可以是 'hash'
routes
})
Vue Router 4 当前写法:
import {
createRouter,
createWebHashHistory,
createWebHistory
} from 'vue-router'
const router = createRouter({
history: createWebHistory(), // 或 createWebHashHistory()
routes
})
说说 Vue Router 的 hash 模式
hash 模式的 URL 可能是 https://example.com/#/vue。修改 hash 时,浏览器通常不会向服务器请求新的页面资源,但会产生历史记录,因此前进和后退可以触发路由变化。Vue Router 通过 hashchange 及相关路由逻辑响应变化。
它兼容性较好、服务器配置简单,但 URL 包含 #,并且需要结合站点部署和搜索引擎策略评估 SEO。不能简单说 hash 是所有 Vue 项目的“默认模式”:Router 3 的默认模式是 hash,而 Router 4 要显式选择 createWebHashHistory() 或 createWebHistory()。
$route 和 $router 的区别
在 Vue 2 / Router 3 或 Vue 3 Router 4 的 Options API 中:
$route是当前路由信息对象,包含path、params、query、hash、fullPath、matched、name等。$router是路由器实例,提供push、replace、go、back等导航方法以及全局守卫注册能力。
Vue Router 4 的 Composition API 中使用:
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
router.push({ name: 'user', params: { id: 1 } })
console.log(route.params.id)
监听路由时,通常监听 () => route.params.id 等具体字段,而不是把整个 route 对象作为深度数据源。
Vuex 是什么?核心流程?
Vuex 是 Vue 2/3 时代的集中式状态管理方案。Vuex 3 通常对应 Vue 2,Vuex 4 对应 Vue 3。它的核心是响应式 store,状态通过约定的流程更新:
组件 -> dispatch Action -> commit Mutation -> State
^ |
|-------------- 响应式渲染 ---------|
state:集中保存状态。getter:从 state 派生数据。mutation:同步修改 state,是 Vuex 追踪状态变化的约定入口。action:可以执行异步工作,再提交 mutation。

Vuex 的规则有助于追踪状态变化,但并不代表任何项目都必须使用它。Vuex 当前处于维护状态,新项目通常优先考虑 Pinia。Vue 2.7 旧项目若使用 Pinia,应按 Pinia 官方兼容说明选择 v2;Pinia v3 面向 Vue 3。
Vuex 中 action 和 mutation 的区别
- Mutation 专注于同步修改 state,提交方式是
store.commit()。 - Action 可以执行异步请求,提交 mutation 的方式是
context.commit(),也可以分发其他 action。 - Action 的
context不是 state 的父级,它是一个包含state、getters、commit、dispatch等能力的上下文对象。
const store = new Vuex.Store({
actions: {
async fetchUsers({ commit, state, getters, dispatch }) {
const users = await fetchUsersFromApi()
commit('setUsers', users)
}
}
})
Redux 和 Vuex 的区别及共同思想
共同点包括:
- 尽量保持单一、可追踪的状态来源。
- 状态变化通过约定的流程发生,便于调试和回放。
- 将共享状态从组件局部逻辑中抽离。
Vuex 的 mutation/action 是 Vue 生态的约定;Redux 通常通过 reducer 生成新 state,并由订阅机制驱动更新。Vuex 的组件更新可以直接利用 Vue 响应式系统,不需要开发者手动订阅 DOM。两者都不是 MVVM 的“实现本身”,而是状态管理方案。
Vuex 和 localStorage 的区别
- Vuex 状态主要存在内存中,刷新页面后通常会丢失。
localStorage是浏览器持久化存储,只能保存字符串,读写需要JSON.stringify()/JSON.parse(),并且是同步 API。- Vuex/Pinia 能参与响应式更新;
localStorage本身不会自动让组件重渲染。 - 持久化状态通常需要在 store 初始化时读取,并在变化时写回;要评估容量、隐私、XSS 和多标签页同步问题。
不能笼统地说“读取内存一定比读取硬盘快”,浏览器存储还有序列化、同步阻塞和实现差异;性能应以实际测量为准。
Vue 3 有什么更新?
Vue 3 的重要变化包括:
- 使用 Proxy 为
reactive()提供对象级响应式,能够处理属性新增/删除、数组索引和length等操作。 - 支持更自然的 Composition API 和
<script setup>,也仍然支持 Options API。 - 支持 Fragment、多根节点、Teleport 和 Suspense 等能力。
- 提供更细粒度的 tree-shaking、静态提升、patch flags 和 block tree 优化。
- 组件、指令、插件等全局 API 从 Vue 构造函数迁移到 app 实例。
原文中的“Vue 3 改成了类式组件”是错误说法。Vue 3 推荐 Composition API 或 Options API,并没有把组件统一改成 class 写法。
Portal 也不是 Vue 3 的内置名称;Vue 3 使用 <Teleport> 把内容渲染到 DOM 的其他位置。
Object.defineProperty 和 Proxy 的区别
Vue 2:
- 初始化时遍历已有属性并安装 getter/setter。
- 新增/删除属性、数组索引和
length存在响应式边界。 - 需要
Vue.set、Vue.delete或包装数组方法补足限制。
Vue 3:
- Proxy 代理整个对象,可以拦截读取、写入、删除、
in、枚举等操作。 reactive()访问嵌套对象时会继续提供深层代理。- 支持 Map、Set 等集合类型。
- 原始对象绕过代理、直接解构 primitive 属性、使用
markRaw等情况仍有使用边界。
对虚拟 DOM 的理解
Virtual DOM 是描述 UI 结构的 JavaScript 数据。状态变化后,框架会生成或更新 VNode,并把必要的差异应用到真实 DOM。它带来声明式渲染、跨平台抽象和编译优化的基础,但不应简单宣传为“任何场景都比手写 DOM 快”或“必然减少重排重绘”。
Vue 3 还会通过静态提升、patch flags、block tree 等编译和运行时优化减少不必要的工作;实际性能应结合 DOM 规模、组件结构和用户操作测量。
虚拟 DOM 和 Diff 的基本过程
简化过程如下:
- 根据状态生成新的 VNode 树。
- 将新旧 VNode 按节点类型、key 和位置进行比较。
- 对可复用节点更新属性和子节点。
- 创建新增节点、卸载删除节点,并将结果应用到真实 DOM。
Vue 2 的 sameVnode、patchVnode、updateChildren 等源码是历史实现;Vue 3 还增加了编译器生成的静态节点和 patch flags。Diff 通常只比较同层级节点,使用启发式策略把复杂度从通用树编辑问题降低到可接受范围,但这不等于所有更新都是严格 O(n)。
为什么要设置 key?
key 用于标识同一列表项或条件分支的身份,帮助 Vue 在更新时判断节点能否复用:
<input v-if="editing" key="edit" />
<input v-else key="view" />
在 v-for 中,建议为每一项提供稳定且唯一的业务 key:
<li v-for="user in users" :key="user.id">
{{ user.name }}
</li>
key 不只是“让 Diff 更快”,更重要的是它影响组件实例、DOM 状态和输入值的复用身份。没有必要为了强制重建而随意使用随机 key,因为随机 key 会让节点每次都被卸载重建。
为什么不建议用 index 作为 key?
当列表会插入、删除、排序或包含输入框、组件内部状态时,index 会随着位置变化,导致 Vue 把旧位置的状态复用给另一条数据。使用稳定业务 id 更安全。
如果列表只会在末尾追加、顺序永远不变且没有内部状态,index 可能不会产生问题;因此不能绝对说 index “和没写 key 完全一样”,但它不应作为可变列表的默认选择。
编译过程
现代 Vue SFC 通常在构建阶段由 @vue/compiler-sfc 解析:
- 解析
<template>、<script>和<style>。 - 将模板转换为 AST,并进行静态节点分析和优化。
- 生成 render 函数及 patch flags 等编译提示。
- 处理脚本、样式和资源,交给构建工具输出代码。
Vue 2 的模板编译实现和 Vue 3 的编译器细节不同,不能把模板编译概括成“主要依靠各种正则表达式替换”。
nextTick 原理和用途
Vue 会批量合并同一轮同步代码中的响应式更新,然后异步执行组件更新。nextTick() 返回一个 Promise(也支持回调),用于等待这轮 DOM 更新完成:
import { nextTick, ref } from 'vue'
const visible = ref(false)
visible.value = true
await nextTick()
// 此时可以读取本轮更新后的 DOM
Vue 2 的历史实现会根据环境选择 Promise、MutationObserver、setImmediate 或 setTimeout 等降级方案;这属于内部调度细节。Vue 3 应关注“等待批量 DOM 更新”这个公共语义,不要依赖具体宏任务/微任务实现。
小结
Vue 2 的知识点仍适合维护旧项目,但必须标注版本边界:Vue.set、Vue.use、beforeDestroy、实例 EventBus 和 Vue Router 3 都不是 Vue 3 的当前写法。新项目应优先使用 Vue 3、Composition API、Router 4、Pinia、useRoute/useRouter 和 onBeforeUnmount/onUnmounted。
官方参考
- Vue 3:计算属性
- Vue 3:侦听器
- Vue 3:生命周期 API
- Vue 3:响应式基础
- Vue 3:渲染机制
- Vue 3:列表渲染
- Vue 3:事件 API 迁移
- Vue 3:v-if 与 v-for 迁移
- Vue 3:KeepAlive
- Vue Router 4:History 模式
- Vue Router 4:Composition API
- Vuex
- Pinia
- Vue 2:响应式原理(历史)
- Vue 2:生命周期(历史)
作者:牛客668579748号
原文链接:https://www.nowcoder.com/discuss/515602762281893888
来源:牛客网。