Vue 全栈面试题大全(2026 最新版最详细)

一、Vue 基础概念篇

1. Vue 的核心思想是什么?

Vue 的核心思想是 数据驱动组件化。数据驱动是指开发者只需维护数据状态,数据变化后页面自动更新;组件化是指将复杂页面拆成多个可复用的小模块,如商品卡片、搜索栏、弹窗表单等。5 个核心特性包括:数据驱动视图(MVVM 思想)、组件化开发、双向数据绑定(v-model)、虚拟 DOM 和指令系统。

1)数据驱动:你只管改数据,页面自己会变

想象你在做一个在线购物车

  • 传统做法:点击"增加数量" → 找到商品数量那个数字的 DOM 元素 → 手动改成新数字 → 再去改总价那一行 → 还要改底部合计...... 一步错,整个页面就乱。

  • Vue 的做法:你只需要改 JS 里的 item.count = 5,然后页面里所有用到 item.count 的地方(数量、总价、底部角标)全部自动刷新

你就像一个指挥官,只需要下命令"数据改成这样",不用自己亲自去搬砖改 DOM。

这就是 数据驱动:数据是唯一的真相来源,页面是数据的"投影"。

2)组件化:像乐高积木一样搭页面

还是购物车页面:

  • 顶部 → 搜索框组件

  • 中间 → 商品卡片组件(被循环用了 10 次)

  • 底部 → 结算栏组件

每个组件就像一块乐高积木,有自己的样式、自己的数据、自己的行为。

  • 优点1:复用 -- 商品卡片可以同时出现在购物车、收藏页、推荐页。

  • 优点2:好维护 -- 购物车的结算栏出 bug,只改结算栏组件,不影响其他地方。

  • 优点3:多人协作 -- 你写商品卡片,我写搜索框,互不打架。

特性 通俗解释
数据驱动视图 上面讲过了,改数据 = 改页面,不用手动操作 DOM
组件化开发 页面拆成一个个独立的小积木,拼起来就是完整应用
双向数据绑定(v-model) 表单输入框里打字 → 变量自动变;变量被代码改了 → 输入框的显示也自动变。就像两个人对着镜子,你笑镜子里的你也笑,镜子里的你笑你也笑。典型场景:搜索框 + 搜索结果实时联动
虚拟 DOM Vue 不在真实 DOM 上改来改去,而是在内存里画一个"草稿"(虚拟 DOM),对比新旧草稿的差异,最后只把改动的地方真正画到页面上,省时省力
指令系统 就是 Vue 给 HTML 开的"外挂",比如 v-if(要不要这个元素)、v-for(循环生成列表)、v-on:@click(点击时干啥),让 HTML 拥有了操控数据的能力

举例:

你用 Vue 写一个点赞按钮:

  • 数据:liked: false, count: 10

  • 模板:<button @click="liked = !liked">{``{ liked ? '已赞' : '点赞' }} {``{ count }}</button>

点击按钮 → 你只改了 liked 变量(数据驱动 ) → 页面自动重新渲染(虚拟 DOM 计算差异 ) → 按钮文字从"点赞 10"变成"已赞 10"(视图更新)。

整个过程,你没有写一行 DOM 操作代码,全是 Vue 帮你干了。

2. 什么是 MVVM 模式?Vue 如何体现 MVVM?

MVVM 是 Model-View-ViewModel 的缩写:Model 是数据层(data、接口数据),View 是视图层(DOM),ViewModel 是连接两者的桥梁。Vue 的 ViewModel 对应 Vue 实例,数据变化通过 ViewModel 自动更新视图,视图操作(如输入)也会通过 ViewModel 同步到数据,实现数据和视图的解耦。

1)Model(数据层)------ 就是"仓库里存的数字"

  • 就是 JS 里的数据:price = 100(单价),count = 2(数量),total = 200(总价)。

  • 它只负责存数据,不知道也不关心页面上怎么显示。

2) View(视图层)------ 就是"用户看到的界面"

  • 就是你在浏览器里看到的输入框(显示数量 2)、一段文字(显示总价 200 元)。

  • 它只负责展示和接收用户操作,不知道这些数字从哪里来。

3) ViewModel(桥梁层)------ 就是"自动传话的小秘书"

  • 在 Vue 里,这个"小秘书"就是 Vue 实例 (你写的 new Vue({...})createApp)。

  • 它的工作:

    • 当用户在输入框里把数量从 2 改成 3 → 小秘书自动把 count 数据改成 3 → 然后自动重新计算 total = 100 * 3 = 300 → 最后自动刷新页面上的总价。

    • 反过来,如果别的代码把 count 直接改成 5 → 小秘书也会自动更新输入框里的显示和总价。

你完全不需要写任何代码去操作 DOM (比如 document.getElementById('total').innerText = 300)。全部由这个小秘书(ViewModel)自动完成。

Model (数据) ViewModel (Vue实例) View (页面)

| | |

price=100 监听数据变化 显示总价: 200

count=2 ──数据变了自动通知──→ │ ──自动更新DOM──→ │

total=200 ↑ ↑

│ │

用户输入3 ──自动改数据←─────────────┘

Vue 是如何体现 MVVM 的?

MVVM 角色 在 Vue 中对应的东西 通俗解释
Model data 里定义的变量,比如 count: 2 就是存放数据的仓库
View 模板(template)里的 HTML,比如 {``{ total }}<input v-model="count"> 就是你眼睛看到的页面样子
ViewModel Vue 实例(new Vue(...)createApp 那个"看不见的手",自动帮你把数据和页面同步起来

最关键的一点:ViewModel 完全接管了 View 和 Model 之间的通信。你只需要关注数据(Model)和页面长什么样(View),剩下同步的脏活累活全由 Vue(ViewModel)自动干了。

★ 和传统开发对比,你就明白 MVVM 的好处了

传统 jQuery 做法(没有 ViewModel):

// 用户改了输入框,你得手动找到所有相关的地方去更新

$('#countInput').on('change', function() {

let count = $(this).val();

$('#totalSpan').text(price * count);

// 如果还有折扣、运费...... 每个地方你都得手动更新

});

Vue 的 MVVM 做法

html 复制代码
<!-- 模板里只需要写绑定 -->
<input v-model="count">
<span>{{ total }}</span>
javascript 复制代码
// JS 里只需要定义数据和计算规则
data: { count: 2, price: 100 },
computed: {
  total() { return this.price * this.count; }
}
// 完毕。任何一方变化,另一方自动跟着变。

MVVM 就是请了一个全自动管家(ViewModel):你只管把数据(Model)和房间布置(View)告诉它,剩下所有"数据变了要擦桌子"、"桌子被动了要改账本"的杂活,它全包了。Vue 就是这样一个管家。

3. Vue 中组件的 data 为什么必须是函数,而根实例可以是对象?

组件是可复用的,若 data 是对象,多个组件实例会共享同一个对象,修改一个就会影响所有实例。将 data 设为函数,每次创建组件时返回新对象副本,确保每个实例的数据独立。根实例唯一且不会被复用,所以可以直接使用对象。

先想一个问题:为什么"组件"容易打架,而"根"不会?

1)根实例 ------ 像公司里唯一的"总经理记录本"

Vue 根实例只在页面启动时创建一次 ,整个页面只有这一个。

它就像总经理手里的唯一一个笔记本 ,总经理本人写写画画,不会有第二个人来抢着改。

所以根实例的 data 可以直接给一个对象(笔记本) → data: { count: 0 },没问题。

2)组件 ------ 像发给每个员工的"一模一样的笔记本"

假设你定义一个"计数器组件",打算在页面里用 3 次(三个不同的计数器)。

错误写法 (把 data 写成对象):(!错误的!)

javascript 复制代码
// 像这样只复印了一份"笔记本模板",但发给三个人的是同一本真笔记本
data: { count: 0 }

结果:三个人共用同一本实体笔记本。第一个人在上面写"count = 1",翻到第二个人那里也变成了 1,第三个人也看到 1。他们互相干扰,完全没法独立计数。

正确写法(把 data 写成函数,每次都返回新对象):

javascript 复制代码
data() {
  return { count: 0 }
}

这就像:你给每个人单独复印了一本空白的笔记本 。第一个人在自己的笔记本上写 1,不影响第二个人;第二个人写 5,不影响第三个人。每个组件实例都有自己独立的数据副本

javascript 复制代码
// data 是函数,每次都返回新对象
function getData() {
  return { count: 0 }
}

let component1Data = getData()  // { count: 0 }
let component2Data = getData()  // 另一个全新的 { count: 0 }

component1Data.count++
console.log(component1Data.count)  // 1
console.log(component2Data.count)  // 0 ✅ 互不影响

一个更形象的比喻:餐馆的点菜单

  • 根实例 :老板手里有一张总菜单(唯一),上面写"今日例汤:排骨汤"。老板自己改没问题。

  • 组件 :每个客人面前都有一张点菜单 (多份)。

    如果所有客人共用同一张纸,A 客人在上面勾了"鱼香肉丝",B 客人再看发现上面多了个勾,菜被改了。

    正确的做法:每位客人拿到自己独立的菜单,随便勾,不影响别人。

为什么根实例可以是对象?(一句话总结)

因为根实例只有一个,不存在"复制多份"的问题,所以可以直接用一个对象。

为什么组件 data 必须是函数?(一句话总结)

因为组件会被复用多次 ,为了避免多个组件实例共用同一份数据而互相干扰,必须让每个实例都有自己独立的数据副本,所以要用函数每次返回新对象。

面试时你可以这样简单说(通俗版)

"根实例像公司唯一的老板,只有一个笔记本,直接写对象就行。组件像很多个员工,每人需要自己的笔记本,如果大家共用一本就会乱套。所以组件 data 必须是一个函数,每次调用都发一本新笔记本,确保各用各的。"

4. Vue 的单向数据流指什么?有什么好处?

单向数据流是数据只能从父组件流向子组件,子组件不能直接修改父组件传递的 props,若要修改必须通过事件通知父组件自行更改。好处是:数据流向清晰易调试,避免子组件随意修改导致状态混乱,提升组件的可维护性。

**场景:**老板和员工的"任务单"

假设你是一家公司的老板,手下有几个员工。你要分配任务给员工。

1)数据的流向:老板 → 员工(父 → 子)

老板(父组件)给每个员工发一张任务单(props),上面写着:

  • 任务名称:"设计海报"

  • 截止日期:"周五"

  • 预算:"500 元"

员工(子组件)只能这张任务单(读数据),用来执行自己的工作。

2)员工不能擅自修改任务单(不能直接改 props)

如果员工觉得"500 元不够",他能直接拿笔把任务单上的"500"改成"1000"吗?

不能! 因为那是老板发出的正式文件,员工没有权限改。擅自修改会乱套:

  • 别的员工看到同一任务单也会变成 1000

  • 老板手里没有记录,财务对不上账

  • 整个公司管理混乱

  1. 员工想改怎么办?必须通知老板(通过事件)

员工写一份申请单($emit 事件) ,比如:"老板,预算 500 元不够,需要增加到 1000 元。"

老板收到后,自己决定是否批准。如果批准,老板自己修改任务单 (父组件修改数据),然后重新发一份新的给员工。

这样,一切修改都由**源头(父组件)**统一进行。

javascript 复制代码
// ❌ 错误做法:子组件直接改 props(不允许)
props: ['budget'],
methods: {
  increaseBudget() {
    this.budget = 1000  // 报错或无效,Vue 会警告
  }
}

// ✅ 正确做法:子组件发事件,让父组件改
// 子组件
this.$emit('request-increase', 1000)

// 父组件
<ChildComponent :budget="budget" @request-increase="budget = $event" />

单向数据流的好处是什么?还是用公司的例子:

好处 通俗解释
数据流向清晰 所有任务单的改动,都能追溯到老板那里。谁改了数据,一目了然,不会出现"不知道哪个员工偷偷改了"的情况
容易调试 如果数据出错了(比如预算变成了 1000),你只需要检查老板(父组件)的逻辑,不需要在所有员工(子组件)里排查
避免混乱 员工不能自行其是,保证了数据来源唯一,组件之间不互相干扰
提升可维护性 新来的员工(新子组件)只看任务单工作,不会影响别人;老板想改流程,只改自己那里,不用通知所有员工改代码

总结一句话(面试口语版)

"单向数据流就像公司里老板发任务单,员工只能看不能改。想改必须打报告给老板,老板自己修改后再发下来。这样所有数据的修改都有唯一源头,不会乱,好排查,好维护。"

5. Vue 的插值表达式 {``{}} 有哪些使用限制?

① 只能写单个表达式(变量、三元运算、简单计算),不能写语句(if、for、赋值等);② 不能直接访问 $refs$el 等特殊属性,但可访问 data、computed、methods;③ 页面加载时可能闪现 {``{}},可加 v-cloak 解决。

限制1:只能填"一个表达式",不能写"一句话"

通俗理解 :填空题的空格里,只能写一个计算结果,不能写一整段程序。

{``{}} 就像一个计算器:

  • ✅ 允许:{``{ 1 + 1 }} → 显示 2

  • ✅ 允许:{``{ price * count }} → 显示总价

  • ✅ 允许:{``{ isLogin ? '欢迎回来' : '请登录' }} → 显示对应文字

  • ❌ 不允许:{``{ if (isLogin) { return '欢迎' } else { return '登录' } }} → 这是语句(if 语句),不是表达式

  • ❌ 不允许:{``{ for (let i=0; i<10; i++) { sum += i } }} → 循环语句也不行

  • ❌ 不允许:{``{ a = b + 1 }} → 赋值语句也不行

为什么?

Vue 的 {``{}} 只负责取一个值并显示 ,它不是一个 JavaScript 执行环境。你要做复杂逻辑,请去 computedmethods 里写好,然后 {``{ 结果 }}

限制2:不能直接访问 $refs$el 等特殊属性

通俗理解:填空题只能填"公开的、准备好的数据",不能填"内部工具"。

$refs 是 Vue 用来抓取 DOM 元素 的工具,$el 是组件根元素。这些东西是在模板渲染完成后 才有的。

{``{}} 在渲染过程中就会被计算,那时候 $refs 还是空的,所以 Vue 不让你在 {``{}} 里用它们。

你可以在 mounted 生命周期里通过 this.$refs.xxx 去操作,但不要在 {``{}} 里写。

能访问什么?

  • data 里的属性(如 message

  • computed 计算属性

  • methods 里的方法(不推荐,因为每次渲染都会执行,但语法允许)

不能访问什么?

  • $refs$el$root 的一部分内部属性

  • 组件实例上未暴露给模板的私有变量

限制3:页面加载时可能闪现 {``{}},怎么解决?

通俗理解 :网络慢的时候,Vue 还没加载完,页面会先把你写的 {``{ message }} 当成普通文本显示出来,过一会儿才变成真正的内容。这叫"闪一下"。

例子

你写了 <span>{``{ userName }}</span>

如果网络卡顿,用户可能会先看到屏幕上一闪而过的 {``{ userName }} 这几个字符,然后才变成"张三"。

解决办法 :加一个 v-cloak 指令,配合 CSS 把它藏起来。

javascript 复制代码
<style>
  [v-cloak] { display: none; }
</style>

<div v-cloak>
  <span>{{ userName }}</span>
</div>

原理:Vue 加载完成后会自动移除 v-cloak 属性,元素才显示。这样用户就看不到原始的 {``{}} 了。

总结一句(面试用)

"{``{}} 只能填单个表达式 (不能写 if/for/赋值),不能访问 $refs 这类渲染后才有的属性,如果担心加载时闪现 {``{}} 原文,就用 v-cloak 配合 CSS 隐藏。"

6. 怎么理解 Vue 是"渐进式框架"?

渐进式是指 Vue 核心功能轻量,可根据需求逐步添加扩展功能,无需一次性全量引入。基础开发只用核心的数据绑定和组件,需要路由时加 Vue Router,需要状态管理加 Vuex/Pinia,需要工程化加 Webpack/Vite。这种设计适配从小脚本到大型应用的不同场景,学习成本也能循序渐进。

举例:

阶段 做什么 对应 Vue 的功能
第1步 煮泡面(只用锅和水) 只用 Vue 核心:数据绑定、显示文本、处理点击
第2步 学炒鸡蛋(加个平底锅) 加组件系统,把页面拆成几个小模块
第3步 朋友来做客,需要多个菜和上菜顺序 加路由(Vue Router),管理不同页面
第4步 开小饭馆,多个厨师共享食材库存 加状态管理(Pinia),统一管理全局数据
第5步 饭店生意好,需要中央厨房 加工程化(Vite/Webpack),打包、优化、热更新

关键点 :你不需要先学会做满汉全席才能煮泡面。Vue 允许你先只用核心功能写一个简单的网页交互,等项目变大时,再逐步把高级特性"加进来",每一步都不强求你学新东西。

对比:Vue 渐进式 vs 非渐进式框架

特性 渐进式(Vue) 非渐进式(如 Angular 早期)
起步 引入一个 JS 文件,写几行代码就能跑 需要安装 CLI、配置 TypeScript、理解依赖注入
增加功能 需要路由?npm install vue-router 加上 框架自带,但一开始就要学
学习曲线 平缓:核心 → 组件 → 路由 → 状态管理 → 工程化 陡峭:必须全学完才能写一个像样的应用
适用场景 从静态页面的小交互到大型复杂应用都能覆盖 更适合中大型项目,小项目会感觉"杀鸡用牛刀"

一句话总结(面试口语版)

"渐进式就是你可以从 Vue 最核心的数据绑定开始,像吃自助餐一样,需要什么功能再往里加,比如要页面跳转就加 Vue Router,要全局状态就加 Pinia。这样小项目不臃肿,大项目不失控,新手老手都能舒服地用。"

7. v-bind 和 v-model 的区别是什么?

v-bind 是单向绑定,数据影响页面属性,简写为 :;v-model 是双向绑定,页面输入与数据可以互相影响。图片地址、class、style 通常用 v-bind,表单输入通常用 v-model。

1)v-bind ------ 单向广播(数据 → 页面)

就像广播喇叭:广播台(数据)说什么,喇叭(页面属性)就放什么。喇叭只能播放,不能说话。

你用 v-bind 把一个 <span>title 属性(鼠标悬停提示)绑定到 name

<span :title="name">把鼠标放上来看看</span>

  • 页面上的 title 属性会显示"小明"

  • 但如果用户在页面上改了 title (实际上用户改不了属性,只能通过 JS),数据里的 name 并不会变

  • 方向:数据 → 视图(单向)

常见用途

  • :src(图片地址)、:href(链接)、:class:style------这些都是"属性",需要跟着数据变,但用户不会反过来改这些属性。
  1. v-model ------ 双向对讲(数据 ⇄ 页面)

就像对讲机:你说的话传给我,我说的话传给你,两边都能说、都能听。

<input v-model="name">

你用 v-model 绑定一个 <input> 输入框:

  • 数据 name = "小明" → 输入框里显示"小明"

  • 用户删除"小"字,输入"红" → 输入框变成"明红",数据 name 也自动变成"明红"

  • 方向:数据 → 视图 并且 视图 → 数据(双向)

常见用途

  • <input><textarea><select>------这些是表单输入控件,用户会修改里面的值,需要同步回数据。

一个对比表格

对比项 v-bind(广播) v-model(对讲机)
数据流向 单向:数据 → 页面属性 双向:数据 ⇄ 表单输入值
简写 : 没有简写,就是 v-model
什么时候用 图片地址、链接、class、style 等属性需要随数据变化时 输入框、复选框、单选框等表单控件需要与数据实时同步时
能不能用户手动改反向影响数据 不能

一个容易混淆的例子:用 v-bind 也可以让输入框显示数据,但它不会反向更新

<!-- 用了 v-bind 的输入框 -->

<input :value="name">

  • 这个输入框会显示"小明"

  • 但用户修改输入框内容时,name 不会变(因为没有绑定 input 事件)

  • 所以 :value 是 v-bind 的单向用法,只是展示数据;v-model 是 :value + @input 的语法糖。

面试一句话总结

"v-bind 是单向绑定,就像广播,只把数据播到页面属性上;v-model 是双向绑定,就像对讲机,数据改页面、页面改数据,通常用在表单输入框上。"

二、响应式原理篇

8. Vue 2 响应式原理是什么?有哪些缺陷?

Vue 2 响应式核心依托 Object.defineProperty(),对数据对象的属性进行劫持拦截,结合 Dep 依赖收集器与 Watcher 视图监听器,搭配发布-订阅模式实现数据驱动视图更新。组件初始化时递归遍历 data 内所有层级属性,为每个属性添加 getter/setter。读取数据时触发 getter 收集依赖,修改数据时触发 setter 通知 Watcher 执行视图重新渲染。

存在四大核心缺陷:无法监听对象新增/删除的属性(需用 $set)、无法监听数组通过下标修改元素和修改 length 的操作、初始全量递归劫持影响性能、仅能劫持对象已定义属性。

1)Vue 2 响应式原理(通俗版)

场景:你是一个仓库管理员,要确保货架上的商品变化时,电脑上的库存清单自动更新

步骤 1:给每个商品装一个"报警器"

Vue 启动时,会遍历你 data 里定义的所有属性(比如 count: 0price: 100),就像给每个商品贴上感应贴纸Object.defineProperty)。

  • 读取 这个商品(比如页面显示 {``{ count }}),报警器就记下:"有人关心这个商品"。

  • 修改 这个商品(比如 this.count = 5),报警器立刻大喊:"改啦!快刷新页面!"

步骤 2:建立"依赖收集表"

每个商品背后有一个小本子(Dep),上面记着所有关心它的人(Watcher,比如页面上的某个地方用到了 {``{ count }})。

  • 你第一次显示 {``{ count }} 时,报警器就在小本子上记下:"页面需要知道 count 的变化"。

  • 下次你修改 count,报警器翻看小本子,通知所有记在上面的人:"count 变了,你们快更新!"

步骤 3:修改数据时自动更新视图

报警器一通知,页面就重新渲染 {``{ count }},保证你看到的永远是最新的。

一句话总结 Vue 2 响应式原理

"初始化时给 data 里每个属性都装上报警器,读取时记录谁依赖它,修改时通知所有依赖去更新页面。"

2)Vue 2 响应式的四大缺陷(通俗版)

缺陷 1 :无法监听新增删除的属性

例子

你有一个空对象 user: {},Vue 启动时它没有属性,所以没装任何报警器。

后来你写 this.user.name = '张三'(新增属性)------ 没有报警器,Vue 不知道,页面就不会更新。

同样,delete this.user.age(删除属性)也监听不到。

解决办法 :用 Vue.set(obj, 'name', '张三')this.$set,手动给新属性装报警器。

缺陷 2 :无法监听通过下标修改数组元素修改数组 length

例子

你有一个数组 list: ['a', 'b', 'c']

  • 你写 this.list[1] = 'x' ------ 改变了第二个元素,但 Vue 检测不到,页面不会更新。

  • 你写 this.list.length = 0 ------ 清空数组,Vue 也检测不到。

为什么?

因为 Object.defineProperty 只能监听对象属性的 get/set,而数组下标在底层也是属性,但出于性能考虑 Vue 没有对每个下标都劫持。

Vue 只拦截了会改变原数组的 7 个方法(push、pop、shift、unshift、splice、sort、reverse),你调用这些方法时 Vue 能知道并更新视图。

解决办法

  • this.list.splice(1, 1, 'x') 代替 this.list[1] = 'x'

  • this.list.splice(0) 代替 this.list.length = 0

缺陷 3 :初始化时递归遍历所有层级,影响性能

例子

如果你的 data 里有一个很深的对象,比如 10 层嵌套,Vue 会在启动时一口气从第一层递归到第十层,给每一个属性都装上报警器。

如果你的数据量很大(比如表格 1000 行 × 50 列),这个过程会占用较多时间,导致页面启动变慢。

对比 Vue 3

Vue 3 是懒代理,一开始只给第一层属性装报警器(Proxy),当你访问到深层属性时才去代理那一层,性能更好。

缺陷 4 :仅能劫持对象已经存在的属性

例子

你在 data 里写了 { a: 1 },Vue 会给 a 装报警器。

如果你在代码里动态添加 b: 2b 没有报警器,不响应。

而且你无法提前知道用户会加什么属性,所以这是天生限制。

3)总结表(面试用)

缺陷 通俗解释 解决办法
无法监听新增/删除属性 只给一开始就有的属性装报警器,新买的商品没报警器 this.$set / this.$delete
无法监听数组下标修改和 length 变更 通过下标改数组元素相当于"没敲门就换了货",报警器不响 用 splice 等 7 个变更方法,或直接替换整个数组
初始递归遍历影响性能 不管用不用,先给所有深层属性都装上报警器,启动慢 换 Vue 3(懒代理),或优化数据结构
仅能劫持已定义属性 后续新增的属性没有报警器 提前定义好所有可能用到的属性,或使用 $set

面试一句话总结

"Vue 2 的响应式好比提前给家里每样东西贴感应器,但新买的东西没贴就感应不到;而且一开始贴太多会影响启动速度;数组通过下标改元素也感应不到。这些问题在 Vue 3 里用 Proxy 解决了。"

9. Vue 3 响应式原理是什么?为什么用 Proxy 替代 Object.defineProperty?

Vue 3 采用 ES6 原生 Proxy 代理整个目标对象,搭配 Reflect 反射 API 完成数据拦截、依赖收集与视图更新,同时引入懒代理机制优化性能。Proxy 可一次性拦截对象 13 类操作行为,包括属性读取、赋值、删除、数组索引修改和长度变更等;采用懒代理策略,初始化不会递归遍历所有属性,仅在属性被访问时创建代理对象,性能更好,可减少约 50% 的内存占用。

1)Vue 2 的方式:给每个房间的每件物品装报警器

Vue 2 就像物业公司在每个房间的每件家具、每个抽屉上都贴了一个感应器。

  • 你打开冰箱(读取 data.count),感应器就记下来"有人关心冰箱"。

  • 你往冰箱里放饮料(修改 data.count),感应器就通知所有关心的人去更新。

问题

  • 即使你从不进入某个房间,物业也会提前把所有房间的每件物品都贴好感应器(初始化递归遍历),很费时间。

  • 如果你突然搬进来一个新沙发(新增属性),之前没有贴感应器,物业就不知道沙发被人坐了(页面不更新)。

  • 你移动书架上的书(数组下标修改),感应器也不响。

2)Vue 3 的方式:在大楼门口装一个"智能总闸" + "按需贴标"

Vue 3 不再给每个物品贴感应器,而是在整栋大楼的入口装一个智能总闸(Proxy)

  • 任何人想进任何一个房间、碰任何一件物品,都必须先经过这个总闸。

  • 总闸会记录:谁(哪个页面)关心哪个房间的哪件物品。

  • 当物品被修改时,总闸立刻通知所有关心的人去更新。

好处

  1. 不需要提前给每件物品贴感应器:总闸统一拦截,节省启动时间。

  2. 新增房间或新物品也能被监控:因为所有进出都要经过总闸,新搬进来的沙发一进门就被总闸盯上了。

  3. 能监控到更多操作:比如你移动书架的书(数组下标修改)、甚至把整个书架搬走(删除属性),总闸都能发现并通知。

3)代码对比

Vue 2 伪代码(Object.defineProperty)

复制代码
// 必须提前知道有哪些属性,逐个添加 getter/setter
let data = { count: 0, name: '张三' }
Object.defineProperty(data, 'count', {
  get() { /* 收集依赖 */ },
  set(newVal) { /* 通知更新 */ }
})
Object.defineProperty(data, 'name', { /* 类似 */ })
// 如果后来 data.age = 18,新增的 age 没有 getter/setter

Vue 3 伪代码(Proxy)

复制代码
let data = { count: 0, name: '张三' }
let proxy = new Proxy(data, {
  get(target, key) {
    // 如果有人读取任何属性(包括未来新增的),都能拦截到
    // 收集依赖
    return target[key]
  },
  set(target, key, value) {
    // 如果有人修改任何属性(包括新增的),都能拦截到
    target[key] = value
    // 通知更新
    return true
  },
  deleteProperty(target, key) {
    // 甚至能拦截删除属性
    delete target[key]
    // 通知更新
    return true
  }
})
// 即使后来 proxy.age = 18,set 也能捕获到,页面自动更新

4)为什么 Vue 3 要换用 Proxy?(面试总结版)

对比项 Vue 2 (Object.defineProperty) Vue 3 (Proxy)
拦截范围 只能拦截对象已经存在的属性,不能拦截新增/删除 拦截整个对象的所有操作(读、写、删除、遍历等13种)
数组支持 无法监听下标修改和 length 变化,需要 hack 完美支持数组索引、length 的变化
初始化性能 需要递归遍历所有属性,数据量大时慢 懒代理:只在访问时才代理深层对象,启动快
内存占用 每个属性都要创建 getter/setter 闭包,内存占用较高 代理整个对象,内存占用减少约 50%
语法限制 不能监听 Map、Set、WeakMap 等新数据结构 可以代理原生数据结构

"Vue 2 像给每个物品贴感应器,新加的东西就感应不到,而且一次性贴太多会卡。Vue 3 换成在大楼门口装智能总闸(Proxy),任何进出修改都逃不过它的眼睛,还不用提前把所有东西都贴上感应器,性能更好,也支持数组下标和新增属性。"

10. Vue 2 如何检测数组变化?

Vue 2 通过重写数组的 7 个变更方法(push、pop、shift、unshift、splice、sort、reverse)来实现数组响应式,这些方法会在调用时同时触发视图更新。对于数组下标直接赋值或修改 length,Vue 2 无法检测到变化,需要借助 $set 方法。Vue 3 使用 Proxy 后天然支持对数组所有操作的监听。

11. computed 和 watch 的区别是什么?

computed 是计算属性,基于其依赖进行缓存,只有依赖变化时才会重新计算,适合模板中使用的派生数据,必须 return 值。watch 是侦听器,每次监听到数据变化时会执行回调函数,支持异步操作,适合在数据变化时执行副作用(如请求接口、操作 DOM)。性能方面,computed 优于手动 watch。

场景:你要写一个购物车页面

有商品单价 100 元,用户输入数量,你需要显示总价,并且当总价超过 500 元时,弹出一个提示"超过预算"。

1)computed ------ "自动计算器"

就像计算器上的一个公式记忆功能:你把"单价 × 数量"这个公式存进去,计算器会自动记住结果。只要单价或数量没变,你按"显示总价"它直接出结果,不会重新算;只有其中一个变了,它才重新算一次。

Vue 代码

javascript 复制代码
computed: {
  total() {
    return this.price * this.count  // 依赖 price 和 count
  }
}

特点

  • 缓存:依赖没变,多次访问 total 直接返回上次结果,不重新计算。

  • 必须 return 一个值,因为它是"计算"出结果给别人用。

  • 适合派生数据:比如总价、全名、格式化日期、过滤后的列表等。

  • 同步:不能在里面写异步请求(setTimeout、fetch 等),因为需要立即返回一个值。

例子

  • 模板里写 <span>{``{ total }}</span>

  • 只要 countprice 不变,Vue 不会重新计算 total,性能好。

2)watch ------ "监控报警器"

就像你在房间装了一个动作传感器 :你设定"如果总价 > 500 就报警"。传感器不关心总价具体是多少,只关心它什么时候跨过 500 这条线。一旦总价变了,传感器立刻检测,如果条件满足就触发报警(弹提示)。

Vue 代码

javascript 复制代码
watch: {
  total(newVal, oldVal) {
    if (newVal > 500) {
      alert('超过预算啦!')
    }
  }
}

特点

  • 无缓存:只要监听的变量变化,就执行回调,不关心结果是否被使用。

  • 不需要 return ,它是执行副作用(比如发请求、操作 DOM、弹窗、打印日志)。

  • 适合异步操作:可以在里面调用接口、设置定时器。

  • 可以监听到旧值和新值,方便做对比逻辑。

例子

  • 监听到 count 变化 → 重新计算税费并保存到后端

  • 监听到 route 变化 → 重新加载页面数据

一个容易混淆的场景:能不能用 watch 代替 computed?

技术上可以,但不推荐。比如总价完全可以用 watch 这样实现:

javascript 复制代码
data() {
  return { price: 100, count: 2, total: 0 }
},
watch: {
  count: {
    immediate: true,
    handler() {
      this.total = this.price * this.count
    }
  }
}

但这样写啰嗦,而且 total 不会自动缓存,每次 countprice 变化都会执行赋值。用 computed 只需一行且自动优化。


对比表格(口语版)

对比项 computed(自动计算器) watch(监控报警器)
有无缓存 有,依赖不变就不重新计算 无,每次变化都执行回调
必须 return 是,返回计算结果 否,执行副作用
同步/异步 同步 同步或异步均可
典型用途 模板里展示的派生数据(总价、全名、过滤列表) 数据变化时发请求、操作 DOM、弹窗、保存日志
能否手动调用 不能直接调用,它是属性 可以调用 this.$watch 或直接写在 watch 对象里
性能 更优(缓存减少无用计算) 一般(总是执行回调)

"computed 像自动计算器,有缓存,依赖不变就不重算,必须返回一个值,适合模板里用的派生数据;watch 像监控报警器,没缓存,只要数据变化就执行回调,适合做异步或 DOM 操作。能用 computed 的地方优先用 computed,会更快更简洁。"

12. nextTick 的作用和原理是什么?

nextTick 用于在 DOM 更新完成后执行回调,确保能获取到更新后的 DOM。Vue 的 DOM 更新是异步的,数据变化后会将 Watcher 推入队列,在下一个事件循环(tick)中批量执行。nextTick 的实现优先使用 Promise(微任务),其次按降级顺序使用 MutationObserver、setImmediate、setTimeout,保证回调在 DOM 更新后执行。

nextTick 的作用:等 DOM 更新完再干某件事

场景 :你写了一个 Vue 组件,里面有一个 show 控制一个弹窗显示。你点击按钮,把 showfalse 改成 true,然后想立刻获取弹窗的 DOM 元素并让它获取焦点。

javascript 复制代码
this.show = true
// 这里直接 console.log(this.$refs.dialog) 可能拿不到?因为 DOM 还没更新完
this.$nextTick(() => {
  this.$refs.dialog.focus()  // 等 DOM 更新完再执行,保证能拿到
})

通俗解释

你改了数据(show=true),Vue 不会立刻去改 DOM(把弹窗画出来),而是先记下"哦,有人改了 show"。它会等一会儿,把这一轮所有数据改动攒在一起,一次性 更新 DOM。

所以你在数据改动后立刻 想操作新 DOM,是操作不到的(因为还没画出来)。

nextTick 就是让你说:"等 Vue 把所有要画的 DOM 都画完了,再执行我这个函数。"

什么要异步更新 DOM?(也就是为什么不立刻画)

比喻 1:餐厅服务员(Vue)接单

  • 你(开发者)是顾客,点了很多菜:红烧肉、清蒸鱼、炒青菜(连续改了多个数据)。

  • 服务员(Vue)不会每点一道菜就冲进厨房大喊一声,那样厨房会乱套(频繁操作 DOM 性能差)。

  • 服务员会把你这桌点的所有菜记在本子上,等你说"好了,就这些",再一次性把单子交给厨房。

  • 厨房(浏览器)做完菜(更新 DOM)后,你才能吃到(拿到新 DOM)。

nextTick 就相当于你在点完菜后,跟服务员说:"等菜都上齐了,帮我拿一瓶醋。" 服务员会在厨房做完所有菜、端上桌之后,再拿醋过来。

比喻 2:老师批改作业(批量更新)

  • 全班同学(数据变化)一起交作业,老师(Vue)不会改一本就发一本,而是全部改完再统一发回去。

  • 如果某个同学(你)想等老师改完自己的作业后立刻看成绩,他不能说"老师你改完我的就马上告诉我",而是要说"老师,您改完所有人的作业后,顺便把我的成绩短信发给我"。

  • nextTick 就是这个"改完所有人的作业后"的时机。

nextTick 的原理:排队在 DOM 更新之后执行

①. Vue 的更新流程

你修改数据 → Vue 把需要更新的 Watcher(比如页面重新渲染的函数)放进一个队列 → 等当前所有同步代码执行完 → 清空队列,统一更新 DOM → DOM 更新完成后,再执行 nextTick 里的回调。

②.nextTick 是如何实现的?(不用背降级顺序,但要理解思路)

Vue 会尽量使用微任务 (Promise)来实现 nextTick,因为微任务会在 DOM 更新后、宏任务之前立即执行,保证你的回调尽快被调用。

如果浏览器不支持 Promise,再降级到 MutationObserver、setImmediate、setTimeout 等宏任务。

简单理解

nextTick 把你的回调函数"挂在"DOM 更新这一轮事件的最后,就像你排在"更新完 DOM"这个队伍的末尾,等更新一完成就轮到你。

一个直观的例子

javascript 复制代码
data() {
  return { msg: 'Hello' }
},
mounted() {
  this.msg = 'World'
  console.log(this.$el.textContent)  // 可能输出 'Hello'(旧值)
  this.$nextTick(() => {
    console.log(this.$el.textContent) // 输出 'World'(新值,DOM 已更新)
  })
}

解释

  • 第一行改了 msg,Vue 记录要更新 DOM。

  • 第二行立即打印 DOM 内容,此时 DOM 还没更新,所以还是 'Hello'。

  • $nextTick 里的回调被推到"DOM 更新后"执行,所以打印的是 'World'。

面试一句话总结(通俗版)

"nextTick 就像在餐厅点完菜后说'等菜上齐了再帮我拿瓶醋'。Vue 更新 DOM 不是立刻做的,而是等所有数据改动结束后批量更新。nextTick 让你能把一个函数安排在这个批量更新完成后执行,这样你就能安全地拿到更新后的 DOM。"

三、生命周期篇

13. Vue 2 的生命周期钩子有哪些?分别在什么时候执行?

生命周期钩子 对应人生阶段 通俗解释(Vue 组件)
beforeCreate 怀孕但还没出生,没名字、没身份证 组件实例刚创建,data 和 methods 都还没初始化,不能访问数据
created 出生了,有了名字和身体,但还没穿上衣服见人 data 和 methods 已经可用,但 DOM 还没生成($el 不存在)
beforeMount 衣服穿好了,准备出门,但还在屋里 模板编译完成,虚拟 DOM 已生成,但还没挂载到真实 DOM
mounted 已经走到大街上,所有人都能看到你了 组件挂载完成,$el 已插入 DOM,可以访问真实 DOM 元素
beforeUpdate 你要整容了,手术前还没动刀 数据变化了,Vue 准备更新 DOM,但 DOM 还是旧的
updated 整容完成,新面貌示人 DOM 已根据最新数据重新渲染完成
beforeDestroy 即将离开这个世界,还能最后交代几句话 组件即将销毁,还可以清理定时器、解绑事件
destroyed 已经离开,所有东西都被清理 组件销毁,所有监听、子组件都移除
javascript 复制代码
<template>
  <div>{{ message }}</div>
</template>

<script>
export default {
  data() {
    return {
      message: 'Hello'
    }
  },
  beforeCreate() {
    console.log('beforeCreate: 没有 data,没有 methods', this.message) // undefined
  },
  created() {
    console.log('created: data 有了,但 DOM 还没出来', this.message) // 'Hello'
  },
  beforeMount() {
    console.log('beforeMount: 虚拟 DOM 好了,但还没替换真实 DOM')
  },
  mounted() {
    console.log('mounted: 页面已经显示了,可以操作 DOM 了', this.$el)
  },
  beforeUpdate() {
    console.log('beforeUpdate: 数据变了,DOM 还没变', this.message)
  },
  updated() {
    console.log('updated: DOM 更新完毕')
  },
  beforeDestroy() {
    console.log('beforeDestroy: 可以告别了,清理定时器')
  },
  destroyed() {
    console.log('destroyed: 组件已消失')
  }
}
</script>

对于 keep-alive 组件,有 activated 和 deactivated 两个独有的生命周期钩子。

如果组件被 <keep-alive> 包裹(缓存),不会走 beforeDestroy / destroyed,而是走 activated 和 deactivated。

钩子 执行时机
activated 缓存组件被重新显示时(比如从隐藏切回可见)
deactivated 缓存组件被隐藏时(比如切换到其他路由)

比喻:就像你有一个玩具箱(缓存),把玩具放进去(deactivated),下次拿出来玩(activated)不用重新买(不用重新创建)。

面试问题常考点

1)哪个钩子可以访问 data 和 methods

created 以及之后的都可以。beforeCreate 不行。

2)哪个钩子可以访问 DOM?

mounted 及之后(updated 等)。beforeMount 时 DOM 还没挂载。

3)请求数据放在 created 还是 mounted?

两者都可以。created 更早,适合不依赖 DOM 的请求;mounted 适合需要操作 DOM 或保证子组件已挂载的场景。

4)父子组件的生命周期顺序?

父 beforeCreate → 父 created → 父 beforeMount → 子 beforeCreate → 子 created → 子 beforeMount → 子 mounted → 父 mounted。

总结:

"beforeCreate 没数据,created 有数据没 DOM,beforeMount 虚拟 DOM 好了,mounted 真实 DOM 出来了,beforeUpdate 数据变 DOM 还没变,updated DOM 变完了,beforeDestroy 还能清理,destroyed 完全消失。被 keep-alive 缓存时用 activated/deactivated 代替销毁。"

14. Vue 3 的生命周期有哪些变化?

Vue 3 将生命周期钩子改为从 vue 按需导入的组合式函数形式:onBeforeMountonMountedonBeforeUpdateonUpdatedonBeforeUnmount(原 beforeDestroy)、onUnmounted(原 destroyed),还新增了 onRenderTrackedonRenderTriggered 用于调试响应式依赖追踪。生命周期可以多次调用,且需要在 setup 中调用-。

15. 请求接口应该在 created 还是 mounted 中发?

两者都可以。区别在于:created 阶段已经完成数据初始化但 DOM 未挂载,比 mounted 更早执行,适合早期数据获取;mounted 阶段保证 DOM 已就绪,适合需要 DOM 依赖的场景。大多数情况下 created 更常用,因为请求可以更早开始-。

四、组件通信篇

16. Vue 组件间的通信方式有哪些?

Vue 中组件间通信有 8 种常规方式:

通信方式 适用场景
props / $emit 父子组件
ref 父组件获取子组件实例
EventBus(emit/on) 兄弟组件、任意组件
parent/root 访问父组件/根实例
attrs/listeners 跨级传递属性和事件
provide / inject 祖孙组件跨层级注入
Vuex / Pinia 全局状态管理

父子组件通信:父组件通过 props 向子组件传数据,子组件通过 $emit 触发事件向父组件发消息。兄弟组件通信:可通过 EventBus 或 Vuex/Pinia 实现。provide/inject 适合祖孙组件间通信,但非响应式。

17. props 的验证有哪些类型?

props 验证支持 String、Number、Boolean、Array、Object、Date、Function、Symbol 等原生构造函数,还支持自定义验证函数和 requireddefault 配置。

五、Vue Router 篇

18. Vue Router 的核心原理是什么?

Vue Router 的核心是通过监听 URL 变化(hash 模式或 history 模式),根据路由配置匹配到对应组件,通过 <RouterView> 动态渲染。它提供了路由守卫(全局守卫、路由独享守卫、组件内守卫)用于权限控制和跳转拦截。Vue Router 支持动态路由、嵌套路由、命名路由和路由懒加载。

19. 路由懒加载如何实现?

使用动态 import 语法,将不同路由对应的组件分割成独立的代码块,只有当路由被访问时才加载对应组件。配置方式:component: () => import('@/views/Home.vue')

20. 路由守卫有哪些?

  • 全局守卫beforeEach(进入前)、beforeResolveafterEach

  • 路由独享守卫 :配置在路由对象中的 beforeEnter

  • 组件内守卫beforeRouteEnterbeforeRouteUpdatebeforeRouteLeave

六、状态管理(Vuex / Pinia)篇

21. Vuex 的核心概念有哪些?

Vuex 有 5 个核心部分:

概念 作用
State 存放响应式数据
Getters 数据的计算属性(类似组件中的 computed)
Mutations 同步 修改 State 的唯一途径,通过 commit 触发
Actions 处理异步操作,通过 dispatch 触发,最终提交 Mutation
Modules 将 store 拆分成模块,支持命名空间

22. Pinia 和 Vuex 的区别是什么?

Pinia 是 Vue 官方推荐的新一代状态管理库,由 Vue 核心团队成员维护。主要区别:

对比项 Vuex Pinia
mutation 有,必须通过 mutation 修改 无,直接修改 state
TypeScript 支持 需复杂配置 天生完美支持
modules 支持,语法复杂 通过多个 defineStore 实现,更简洁
体积 较大 更小,Tree-shaking 更好
开发体验 较繁琐 代码更少,更直观

23. Pinia 的核心用法?

通过 defineStore 定义 store,可直接修改 state:store.count++,也可以通过 $patch 批量修改。支持 actions 处理同步和异步操作,getters 类似计算属性。支持持久化存储插件-。

七、Vue 3 新特性篇

24. Vue 3 相比 Vue 2 有哪些主要变化?

Vue 3 全面升级,核心变化包括:

维度 Vue 2 Vue 3
响应式 Object.defineProperty,需递归劫持 Proxy,懒代理,性能提升
组件根节点 强制单根节点 支持多根节点(Fragments)
API 风格 Options API Composition API(可选,可共存)
TypeScript 支持较弱 基于 TS 编写,原生深度支持
打包体积 较大 Tree-shaking 后体积减小约 40%
新增特性 Teleport、Suspense、自定义渲染器
性能 基准 更新性能 1.3-2 倍,SSR 速度 2-3 倍

25. Composition API 解决了哪些痛点?

Options API 的局限性:逻辑关注点分散(同一功能的代码被拆分到 data、methods、computed、watch 等不同选项中)、逻辑复用困难(Mixins 存在命名冲突和来源不清晰的问题)、TypeScript 类型推断不友好。Composition API 的优势:逻辑可集中组织(将相关功能放在一起,类似 React Hooks)、更好的 TypeScript 支持(完整类型推断)、更好的逻辑复用(通过自定义组合函数 useXxx)、更小的打包体积(更好的 Tree-shaking 支持)。

26. ref 和 reactive 的区别是什么?

  • reactive:用于创建响应式对象/数组,接收普通对象,返回 Proxy 代理对象,适用于对象类型数据。

  • ref :用于创建响应式基本数据类型(String、Number、Boolean),也支持对象,通过 .value 访问和修改,适用于基本类型或需要整体替换的场景。

  • Vue 3 中同时提供了两种 API,根据场景灵活选择。在模板中使用 ref 时会自动解包,无需 .value

27. Teleport 是什么?有什么应用场景?

Teleport 是 Vue 3 新增的组件,可将组件模板渲染到 DOM 中的任意指定位置(如 body),同时保持组件在逻辑上的归属关系。典型应用场景:全局弹窗系统、通知消息组件、模态对话框,解决在 Vue 组件内部嵌套弹窗时位置、z-index 和样式处理的困难。

28. Vue 3.5 版本有哪些新特性?

Vue 3.5 引入双向链表(Doubly Linked List)和版本计数(Version Counting)机制重构了响应式系统,将"关系"本身实体化为节点,依赖关系不再是隐式映射表,而是可遍历的链表结构,性能提升高达 56%。

八、性能优化篇

29. Vue 项目有哪些常见的性能优化手段?

  • 代码分割与懒加载 :路由组件通过 () => import() 实现按需加载

  • 打包体积优化:开启 Gzip 压缩、使用 CDN 加载第三方库、UI 框架按需引入

  • 运行时优化 :频繁切换用 v-show 代替 v-ifv-for 必须配合唯一 key、避免模板内复杂运算

  • 图片资源优化:图片懒加载、压缩图片、使用字体图标代替小图片

  • 网络优化:启用 HTTP 缓存(Cache-Control)、使用 Service Worker 离线缓存

  • SSR(服务端渲染) :解决首屏加载慢和 SEO 问题

30. v-for 中 key 的作用是什么?

key 的主要作用是为虚拟 DOM 的 diff 算法提供标识,帮助 Vue 精准识别哪些节点是相同的、哪些需要移动或复用。使用唯一且稳定的 key 可以最大程度重用和重新排序现有元素,避免不必要的 DOM 操作,提升性能。如果使用 index 作为 key,在列表动态增删或排序时会产生错误的 DOM 复用,导致状态错乱。建议使用每条数据中唯一且稳定的标识作为 key(如 id)。

31. 首屏加载慢如何解决?

常见解决方案:

  • 路由懒加载和组件懒加载,将入口文件拆分为多个代码块

  • UI 库按需引入

  • 使用 Webpack 的 splitChunks 抽离公共代码

  • 图片资源的压缩和懒加载

  • 开启 Gzip 压缩(compression-webpack-plugin)

  • 静态资源本地缓存(HTTP 缓存、localStorage)

  • 使用 SSR 或预渲染

九、原理与源码进阶篇

32. Vue 的虚拟 DOM 和 Diff 算法原理是什么?

虚拟 DOM 本质是通过 JavaScript 对象树模拟真实 DOM 结构。Vue 模板编译后生成渲染函数(render 函数),执行 render 函数返回 VNode 树,通过 diff 算法比较新旧 VNode 树的差异,最后只将差异部分应用到真实 DOM 上。

Vue 2 采用启发式双端 diff 算法,通过以下策略优化性能:

  • 同层比较策略:仅在同一层级节点间比较,忽略跨层级移动

  • 双端指针遍历:维护 oldStart/oldEnd 和 newStart/newEnd 四个指针,通过四种匹配模式(头头、尾尾、头尾、尾头)快速定位可复用节点

  • key 值优化:通过唯一 key 精准识别可复用节点,避免无序列表的错误匹配

  • 异步渲染队列:使用 nextTick 机制合并多次数据变更,确保同一事件循环中只执行一次完整 diff

Vue 3 在此基础上进一步优化,引入了静态提升(Static Hoisting)、补丁标志(Patch Flags)和树结构打平(Tree Flattening)等编译时优化,大幅减少 diff 范围。

33. Vue 的整体实现流程是什么?

Vue 的整体实现流程分为四个阶段:

  1. 解析模板成 render 函数 :将 .vue 单文件组件中的模板编译为 JavaScript 渲染函数

  2. 响应式监听 :Vue 2 使用 Object.defineProperty,Vue 3 使用 Proxy 对数据进行劫持

  3. 首次渲染:执行 render 函数生成 vnode,触发 getter 收集依赖,通过 patch 函数将 vnode 渲染成真实 DOM

  4. 数据更新:数据变化触发 setter,Dep 通知 Watcher 重新执行 updateComponent,生成新 vnode 并与旧 vnode 进行 diff,仅更新变化部分

34. Observer、Dep、Watcher 三者的关系是什么?

  • Observer :遍历数据对象的所有属性,使用 Object.defineProperty(Vue 2)或 Proxy(Vue 3)将它们转换为 getter/setter,用于依赖收集和派发更新

  • Dep (依赖收集器):每个响应式属性都有一个 Dep 实例,用于收集当前依赖该属性的 Watcher,其内部 subs 数组存放 Watcher 实例。数据变化时通过 dep.notify() 通知所有 Watcher

  • Watcher(观察者):负责执行具体的更新操作(如组件渲染、computed 更新、watch 回调)。当 Dep 通知时,Watcher 会调用其 update 方法执行对应逻辑

  • 三者协作流程:初始化时 Observer 建立响应式系统;页面渲染读取数据时触发 getter,Dep 收集当前 Watcher;数据修改时触发 setter,Dep 通知所有 Watcher 执行更新-

35. v-if 和 v-for 为什么不能同时使用?

在 Vue 2 中,v-for 的优先级高于 v-if,同时使用会导致每次遍历都会执行 v-if 判断,造成性能浪费。推荐先将列表过滤计算后再遍历,或将需要判断的元素放在外层 <template> 中。Vue 3 中 v-if 的优先级高于 v-for,但依然不推荐同时使用。

36. mixins 有哪些缺点?为什么 Vue 3 推荐使用 Composition API?

mixins 的四大痛点-:

  • 来源不明(隐式依赖) :组件中的属性和方法可能来自任意 mixin,难以追踪来源

  • 命名冲突:多个 mixin 定义了相同属性名时,合并规则不直观(后引入的覆盖先前的)

  • 命名空间污染:mixins 内容直接注入组件,容易产生不可预见的覆盖

  • 逻辑来源分散:同一功能的代码分散在多个 mixins 中,维护困难

  • 可复用性受限:与 Vue 组件生命周期紧密耦合,难以在非 Vue 环境中复用

Composition API 通过组合函数(useXxx)替代 mixins,解决了上述所有问题,提供更好的逻辑封装和复用能力。

十、Vue 项目实战场景题

37. 后台管理系统中的权限控制如何实现?

  • 登录后获取用户权限列表(角色/按钮/菜单权限)

  • 根据权限动态生成可访问的路由表,通过 router.addRoutes(Vue 2)或动态添加路由(Vue 3)挂载

  • 路由守卫 beforeEach 中进行权限判断,无权限时跳转 403 页面

  • 按钮级权限通过自定义指令 v-permission 实现,根据权限列表控制按钮显隐

  • 菜单权限通过递归渲染侧边栏,过滤无权限菜单

38. 如何设计可复用的通用组件?

设计可复用组件需遵循:props 单向数据流(父传子)、����事件通知(子传父)、����插槽预留扩展点(默认插槽/具名插槽/作用域插槽)、合理的�����验证和默认值、组件独立性和低耦合、支持'�−�����'双向绑定封装、支持透传'emit事件通知(子传父)、slot插槽预留扩展点(默认插槽/具名插槽/作用域插槽)、合理的props验证和默认值、组件独立性和低耦合、支持'v−model'双向绑定封装、支持透传'attrs`。

39. 如何避免 Vue 项目中的内存泄漏?

及时清理定时器和事件监听(setTimeout/setIntervalbeforeDestroy/beforeUnmount 中清除)、解绑全局事件(window.addEventListener 需对应 removeEventListener)、清理第三方库实例、避免在销毁后的组件中执行异步回调(判断 _isDestroyed 标志)、合理使用 keep-aliveinclude/exclude

40. SPA 如何优化 SEO?

  • 使用 SSR(服务端渲染) ,如 Nuxt.js,服务端直接返回完整 HTML-

  • 使用 预渲染(Prerender) ,对静态页面提前生成 HTML(适用于内容变化少的页面)

  • 配置路由的 meta 信息 和动态 title/description(配合 vue-meta)

  • 确保页面有语义化的 HTML 结构和合理的关键词布局

相关推荐
Aphasia3113 小时前
手写KeepAlive组件
前端·react.js·面试
两个西柚呀3 小时前
js中的同步和异步,三种处理异步任务的方式
前端·javascript
pe7er4 小时前
软件设计不要“既要又要”
前端·后端·架构
kyriewen4 小时前
从Webpack到Vite:我们迁移了一个10万行代码的项目,总结了这7个坑
前端·webpack·vite
IT_陈寒4 小时前
Java Stream并行流的坑:我花了3小时才找到的线程安全问题
前端·人工智能·后端
小新1104 小时前
最简单但完整的 Vue 响应式示例(一个简单的计数器按钮)
前端·javascript·vue.js
川冰ICE4 小时前
JavaScript进阶④|Symbol与元编程,对象的隐藏身份
开发语言·javascript·ecmascript
水煮白菜王4 小时前
开源 AI 桌宠 Clawd on Desk:让 Claude Code 的状态从终端‘蹦‘到桌面
javascript·人工智能·开源
吃口巧乐兹5 小时前
异步异常处理:AggregateException 的拆解与最佳实践
javascript