Vue.js 核心知识点全面解析

1. MVC 和 MVVM 的区别

题目重述

请说明 MVC 与 MVVM 架构模式的区别,并解释 Vue 中为何未完全遵循 MVVM。

详解

MVC(Model-View-Controller) 是一种经典软件设计模式:

  • Model:处理数据逻辑,如数据库操作。

  • View:负责展示数据。

  • Controller:接收用户输入,协调 Model 和 View。

其核心思想是:Controller 将 Model 数据渲染到 View 上,即在控制器中完成数据赋值。

MVVM(Model-View-ViewModel) 在此基础上引入了 ViewModel 层:

  • ViewModel 实现双向绑定:

    • Model 转为 View 显示(数据绑定)

    • View 操作转为 Model 更新(DOM事件监听)

最大区别在于:MVVM 实现了 View 与 Model 的自动同步。当 Model 变化时,View 自动更新,无需手动操作 DOM ------ 这正是 Vue "数据驱动" 的体现。

但官方声明:Vue 并未完全遵循 MVVM

原因在于,严格的 MVVM 禁止 View 与 Model 直接通信,而 Vue 提供了 $refs 可让 Model 直接访问并操作 DOM 元素(View),违反了该原则。

知识点

  • MVVM 架构模式:通过 ViewModel 实现视图与模型的双向绑定,提升开发效率。

  • 数据驱动视图:状态变化自动触发 UI 更新,无需手动操作 DOM。

  • $refs 的作用与限制:提供对 DOM 或子组件实例的直接引用,破坏了 MVVM 的隔离性。

2. 为什么 data 必须是一个函数?

题目重述

在 Vue 组件中,data 为什么要写成返回对象的函数形式?

详解

在 Vue 组件中,多个实例可能复用同一个组件模板。若 data 写为对象:

javascript 复制代码
data: { count: 0 }

则所有组件实例将共享同一份 data 引用,造成"一处修改,处处响应"的问题。

而将其定义为函数:

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

每次创建新实例时都会调用该函数,返回一个全新的独立数据对象,相当于为每个组件创建私有数据空间,避免相互影响。

这是 JavaScript 原型链机制下的必要设计。

知识点

  • 组件实例隔离:确保各组件拥有独立的数据副本。

  • 函数返回对象:实现数据私有化,防止引用共享。

  • JavaScript 闭包应用:利用函数作用域封装内部变量。

3. Vue 组件通讯方式有哪些?

题目重述

列举 Vue 中父子组件、兄弟组件之间的通信方式。

详解

常见的组件通信方式包括:

方式 说明
props / $emit 父传子(props)、子传父($emit)------最基础的方式
$parent / $children 访问父/子组件实例(不推荐深层依赖)
$attrs / $listeners 向下透传未被 props 接收的属性和事件(Vue 2.4+)
provide / inject 跨层级注入数据(适用于高阶组件或库开发)
$refs 获取子组件或 DOM 实例,进行直接调用
eventBus(事件总线) 兄弟组件间通信(小型项目可用,大型建议用 Vuex)
Vuex / Pinia 全局状态管理,适用于复杂状态流

⚠️ 注意:Vue 3 推荐使用 mittemitter 替代 EventBus;Vuex 已逐步被 Pinia 取代。

知识点

  • 单向数据流原则:父组件通过 props 向下传递数据。

  • 事件发射与监听emit 触发事件,on/$off 监听(eventBus)。

  • 跨级依赖注入:provide/inject 实现祖先向后代传值。

4. Vue 生命周期钩子有哪些?一般在哪一步发送请求?

题目重述

列出 Vue 的生命周期钩子函数,并说明异步请求的最佳发起时机。

详解

Vue 实例的完整生命周期如下:

钩子 执行时机 是否可访问 DOM 是否支持 SSR
beforeCreate 实例初始化后,数据观测前
created 实例创建完成,数据已响应式处理 ❌(无 $el)
beforeMount 挂载开始前,render 首次执行
mounted 挂载完成后,可访问真实 DOM ❌(客户端)
beforeUpdate 数据更新前,patch 之前
updated 虚拟 DOM 重新渲染后
beforeDestroy 实例销毁前
destroyed 实例销毁后
activated keep-alive 组件激活时
deactivated keep-alive 组件失活时
异步请求最佳位置:created

理由如下:

  • 数据已初始化,可以安全赋值;

  • mounted 更早获取数据,减少页面 loading 时间;

  • 支持服务端渲染(SSR),beforeMountmounted 在 SSR 中不会执行。

✅ 推荐做法:

javascript 复制代码
created() {
  this.fetchData();
}

知识点

  • created 钩子用途:适合发起网络请求、初始化数据。

  • mounted 钩子用途:适合操作真实 DOM、绑定第三方插件。

  • SSR 兼容性差异:created 是唯一可在服务端运行的数据请求钩子。

5. v-if 与 v-show 的区别

题目重述

比较 v-if 与 v-show 的实现机制与使用场景。

详解

特性 v-if v-show
编译结果 条件不满足时不生成节点(移除 DOM) 总是渲染,通过 display:none 控制显示
切换开销 高(需重建 VNode) 低(仅样式切换)
初始渲染开销 低(懒加载) 高(始终渲染)
适用场景 条件少变、初始不显示 频繁切换

示例:

html 复制代码
<!-- v-if:条件 false 时不渲染 -->
<div v-if="visible">Hello</div>

<!-- v-show:总是存在,仅控制 display -->
<div v-show="visible">Hello</div>

扩展:display:none vs visibility:hidden vs opacity:0

属性 占据空间 子元素继承 触发事件 支持过渡动画
display: none
visibility: hidden
opacity: 0

知识点

  • 条件渲染策略选择:根据频率决定用 v-if 或 v-show。

  • CSS 显示控制差异:理解不同隐藏方式的影响范围。

  • 过渡动画兼容性:只有 opacity 支持 transition 动画。

6. Vue 内置指令有哪些?

题目重述

列举常用的 Vue 内置指令及其功能。

详解

指令 功能
v-once 仅渲染一次,后续不更新
v-cloak 防止页面闪动,配合 CSS 使用
v-bind (:) 动态绑定属性
v-on (@) 绑定事件监听器
v-html 插入 HTML 字符串(注意 XSS)
v-text 更新文本内容
v-model 表单双向绑定(语法糖)
v-if / v-else / v-else-if 条件渲染
v-show 显示/隐藏切换
v-for 列表循环渲染(需加 key)
v-pre 跳过编译,提升性能
v-memo(Vue 3.2+) 缓存子树

⚠️ 安全提示:v-html 若插入不可信内容易导致 XSS 攻击,应过滤或转义。

知识点

  • v-model 本质是语法糖:对 input/checkbox/select 分别绑定 value/input 或 checked/change。

  • v-cloak 解决闪烁问题 :结合 [v-cloak] { display: none } 使用。

  • key 的重要性:v-for 中必须提供唯一 key 以提高 diff 效率。

7. 如何理解 Vue 的单项数据流?

题目重述

解释 Vue 的"单项数据流"原则及其意义。

详解

Vue 遵循 父组件 → 子组件 的单向数据流:

  • 数据只能由父级通过 props 传给子组件;

  • 子组件不能直接修改 props,否则会警告。

这样做的目的是防止子组件意外改变父组件状态,导致数据流混乱,难以追踪 bug。

正确做法:

  • 若需本地修改,应在 data 中复制 prop 值;

  • 修改后通过 $emit 通知父组件更新源数据。

反例 ❌:

TypeScript 复制代码
<Child :value="msg" />
<!-- 子组件中直接 this.value = 'new' 会报错 -->

正例 ✅:

javascript 复制代码
data() {
  return {
    localValue: this.value // 复制一份
  }
},
methods: {
  handleChange(val) {
    this.$emit('update:value', val); // 通知父组件
  }
}

知识点

  • 单向数据流原则:保证数据流向清晰可控。

  • props 不可变性:禁止子组件直接修改父级传递的数据。

  • 事件通知机制:通过 $emit 回传变更请求。

8. computed 和 watch 的区别及应用场景

题目重述

对比 computed 与 watch,并说明各自的使用场景。

详解

对比项 computed watch
是否缓存 ✅ 有缓存,依赖不变不重新计算 ❌ 每次变化都执行
是否同步 ✅ 同步执行 ✅ 同步,但可异步操作
适用场景 基于现有数据派生新值 响应数据变化执行副作用(如请求、定时器)
computed 示例:
javascript 复制代码
computed: {
  fullName() {
    return this.firstName + ' ' + this.lastName;
  }
}
watch 示例:
javascript 复制代码
watch: {
  searchQuery(newVal) {
    if (newVal.length > 2) {
      this.search(); // 发起搜索请求
    }
  }
}

✅ 推荐:模板中展示的衍生数据用 computed;需要执行异步或复杂逻辑时用 watch

知识点

  • 计算属性缓存机制:仅当依赖变化才重新求值。

  • 侦听器用于副作用:适合处理异步任务或资源清理。

  • setter 可配置:computed 可设置 getter/setter 实现双向绑定。

9. v-if 与 v-for 为什么不建议一起使用?

题目重述

解释 v-if 和 v-for 不应同时使用的根本原因。

详解

Vue 的模板解析顺序是:先解析 v-for,再解析 v-if

这意味着即使某些项不符合 v-if 条件,仍会被遍历执行,造成性能浪费。

例如:

html 复制代码
<li v-for="user in users" v-if="user.active">
  {{ user.name }}
</li>

→ 所有用户都会被遍历,但只渲染 active 的。

✅ 正确做法:使用 computed 预先过滤:

javascript 复制代码
computed: {
  activeUsers() {
    return this.users.filter(u => u.active);
  }
}
html 复制代码
<li v-for="user in activeUsers">
  {{ user.name }}
</li>

此外,v-for 应搭配唯一的 key,避免使用 index 作为 key。

知识点

  • 指令优先级问题:v-for 优先于 v-if 编译。

  • 性能优化手段:用计算属性提前过滤列表。

  • key 的正确用法:必须使用唯一标识(如 id),而非 index。

10. Vue 2.0 响应式数据的原理

题目重述

描述 Vue 2.x 响应式系统的实现原理。

详解

Vue 2 使用 Object.defineProperty 实现数据劫持 + 发布订阅模式

核心流程如下:

1.数据劫持 :遍历 data 中的所有属性,使用 Object.defineProperty 为其添加 get 和 set。

javascript 复制代码
Object.defineProperty(obj, 'key', {
  get() { /* 依赖收集 */ },
  set(newVal) { /* 派发更新 */ }
});

1.依赖收集 :在 getter 中将当前 Watcher 添加到 Dep(依赖收集器)中。

2.派发更新 :当数据变化时,在 setter 中通知所有订阅者(Watcher)更新视图。

3.观察者模式结构

  • Observer:劫持对象属性

  • Dep:每一个属性对应一个依赖收集器

  • Watcher:具体更新行为(渲染函数、computed 等)

⚠️ 局限性:

  • 无法检测对象新增属性或删除属性;

  • 数组索引修改和 length 变更无法监听;

  • 需通过 Vue.set 手动添加响应式属性。

知识点

  • Object.defineProperty 缺陷:无法监听动态增删属性。

  • 依赖收集机制:getter 中收集 watcher,setter 中通知更新。

  • 观察者模式三要素:Observer、Dep、Watcher 协同工作。

11. Vue 如何检测数组变化?

题目重述

解释 Vue 为何不能监听数组索引变化,以及如何解决。

详解

由于性能考虑,Vue 没有对数组每一项进行 defineProperty 劫持 ,而是通过 重写数组原型上的 7 个变异方法 来实现监听:

javascript 复制代码
const methodsToPatch = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];

这些方法被拦截后,会在原有功能基础上触发视图更新。

因此以下操作不会触发更新:

javascript 复制代码
this.list[0] = 'new';  // ❌ 不会触发
this.list.length = 0;  // ❌ 不会触发

✅ 正确做法:

javascript 复制代码
this.$set(this.list, 0, 'new');           // ✔️
this.list = this.list.splice(0, 1, 'new'); // ✔️

或者使用 Vue.set(array, index, value)。

知识点

  • 数组变异方法重写:通过 AOP 思想劫持 push/pop/splice 等方法。

  • 索引赋值无法监听:因 defineProperty 未对数组索引做代理。

  • Vue.set 强制响应式:用于添加新属性或更新数组元素。

12. Vue 3.0 了解多少?相比 2.0 有何改进?

题目重述

简述 Vue 3 的主要新特性及优势。

详解

主要变化:

维度 Vue 2 Vue 3
响应式原理 Object.defineProperty Proxy
API 风格 Options API Composition API(setup)
模板语法 slot="xxx" <template #header>
v-model 只能绑定 value 支持多个 v-model、自定义修饰符
Fragment 不支持多根节点 支持 <template> 多根节点
Tree-shaking 部分支持 更优,按需打包
TypeScript 支持 较弱 原生支持

核心升级点:

  • Proxy 代替 defineProperty:可监听对象新增属性、数组索引变化等。

  • Composition API:逻辑复用更灵活(类似 React Hooks)。

  • Teleport 组件:将内容渲染到 DOM 任意位置(如弹窗)。

  • Suspense:异步组件加载状态管理。

  • 性能提升:更快的 diff、静态提升、block tree 优化。

setup 示例:

javascript 复制代码
setup(props, { emit }) {
  const count = ref(0);
  const double = computed(() => count.value * 2);

  function increment() {
    count.value++;
    emit('add');
  }

  return { count, double, increment };
}

知识点

  • Proxy 优势:支持动态属性监听、数组完整监控。

  • Composition API 优势:更好的逻辑组织与复用能力。

  • Tree-shaking 支持:未使用模块不被打包,减小体积。

13. Vue 3 与 Vue 2 响应式原理区别

题目重述

对比 Vue 2 与 Vue 3 响应式实现机制的不同。

详解

特性 Vue 2 Vue 3
核心 API Object.defineProperty Proxy
监听范围 仅已有属性 新增/删除属性也可监听
数组监听 重写 7 个方法 Proxy 全面拦截
性能表现 初始化递归劫持耗时 惰性代理,性能更好
兼容性 IE9+ IE 不支持(Proxy)

Vue 3 使用 Proxy 包装整个对象,能拦截更多操作(共13种 trap),如 in、deleteProperty、ownKeys 等。

javascript 复制代码
const reactive = (obj) => {
  return new Proxy(obj, {
    get(target, key) {
      track(target, key); // 依赖收集
      return Reflect.get(...arguments);
    },
    set(target, key, value) {
      const res = Reflect.set(...arguments);
      trigger(target, key); // 派发更新
      return res;
    }
  });
};

知识点

  • Proxy 更强大:可监听动态属性增删。

  • Reflect 配合使用:保持默认行为一致性。

  • track/trigger 机制:Vue 3 中依赖收集的新实现方式。

14. Vue 父子组件生命周期执行顺序

题目重述

描述 Vue 加载、更新、销毁过程中父子组件生命周期的执行顺序。

详解

✅ 加载过程:

父 beforeCreate → 父 created → 父 beforeMount

→ 子 beforeCreate → 子 created → 子 beforeMount → 子 mounted

→ 父 mounted

✅ 子组件更新过程:

父 beforeUpdate → 子 beforeUpdate → 子 updated → 父 updated

✅ 父组件更新过程:

父 beforeUpdate → 父 updated

✅ 销毁过程:

父 beforeDestroy → 子 beforeDestroy → 子 destroyed → 父 destroyed
💡 提示:mounted 钩子中才能访问子组件的 DOM 或实例。

知识点

  • 自上而下挂载,自下而上完成:父先准备,子先挂载,父最后完成。

  • 更新顺序一致:父触发更新,子响应更新。

  • 销毁逆序执行:先子后父,保障资源释放有序。

15. 虚拟 DOM 是什么?优缺点是什么?

题目重述

解释虚拟 DOM 的概念及其利弊。

详解

Virtual DOM 是用 JS 对象模拟真实 DOM 结构,是对 DOM 的抽象描述。

例如:

javascript 复制代码
{
  tag: 'div',
  children: [
    { text: 'Hello' }
  ],
  data: { id: 'app' }
}

Vue 在数据变化时,先生成新的 VNode,然后通过 diff 算法 对比新旧 VNode,找出最小变更集,最后批量更新真实 DOM。

优点:

  1. 避免频繁操作 DOM:JS 层计算变更,减少重排重绘。

  2. 提升开发体验:无需手动操作 DOM,专注数据逻辑。

  3. 跨平台能力:VNode 可渲染到 Web、Weex、SSR 等环境。

缺点:

  1. 首次渲染慢于 innerHTML:多了一层 VNode 构造与 diff 计算。

  2. 无法极致优化:框架通用性牺牲部分性能。

知识点

  • snabbdom 库借鉴:Vue 2 虚拟 DOM 基于 snabbdom 实现。

  • diff 算法核心:同层比较、key 优化、双指针对比。

  • 批量更新策略:合并多次变更,减少 DOM 操作次数。

16. v-model 原理

题目重述

解释 v-model 的底层实现机制。

详解

v-model 是语法糖,根据不同的元素类型自动转换为对应的属性绑定与事件监听。

元素类型 绑定属性 监听事件
<input> / <textarea> value input
<checkbox> / <radio> checked change
<select> value change

例如:

html 复制代码
<input v-model="msg">

等价于:

html 复制代码
<input 
  :value="msg" 
  @input="msg = $event.target.value"
>

对于组件:

html 复制代码
<CustomInput v-model="msg" />

等价于:

html 复制代码
<CustomInput 
  :modelValue="msg" 
  @update:modelValue="msg = $event"
/>

⚠️ 注意:中文输入法组合期间不会触发 v-model 更新,避免误输入。

知识点

  • v-model 是语法糖:简化表单双向绑定。

  • 不同元素绑定不同 property:input/value vs checkbox/checked。

  • 组件 v-model 默认使用 modelValue:Vue 3 中可自定义 prop 名。

17. v-for 为什么要加 key?

题目重述

解释 key 在 v-for 中的作用及重要性。

详解

key 是 VNode 的唯一标识,用于 diff 算法判断是否复用节点。

若不加 key,Vue 采用"就地复用"策略:尽可能复用相同类型的节点,可能导致状态错乱。

例如:

html 复制代码
// 初始
[{ id: 1, name: 'A' }, { id: 2, name: 'B' }]
// 删除第一个后
[{ id: 2, name: 'B' }]

没有 key 时,Vue 会认为第二个节点复用第一个位置,导致输入框保留原值。

加上唯一 key 后:

html 复制代码
<li v-for="item in list" :key="item.id">{{ item.name }}</li>

Vue 能准确识别节点身份,避免错误复用。

性能方面,key 可构建 map 映射,加快查找速度。

知识点

  • key 提升 diff 准确性:防止节点错位复用。

  • key 必须唯一且稳定:避免使用 index。

  • map 优化查找性能:利用 key 构建哈希映射。

18. Vue 事件绑定原理

题目重述

解释 Vue 事件绑定的内部机制。

详解

Vue 的事件分为两类:

  1. 原生 DOM 事件

    • 使用 addEventListener 绑定到真实元素;

    • 如 @click.native。

  2. 组件自定义事件

    • 基于 on、emit 实现发布订阅模式;

    • 组件间通过事件通信。

javascript 复制代码
// 子组件
this.$emit('submit', data);

// 父组件
<Child @submit="handleSubmit" />

内部维护一个事件中心(Event Bus),存储事件名与回调映射。

.native 修饰符表示绑定原生事件,否则为组件事件。

知识点

  • 发布订阅模式on/emit 实现组件间通信。

  • 原生事件与组件事件区分:native 修饰符控制绑定目标。

  • 事件委托机制:Vue 在父级监听事件,提高性能。

19. vue-router 路由守卫执行顺序

题目重述

描述完整的 vue-router 导航守卫执行流程。

详解

完整导航流程如下:

  1. 导航被触发

  2. 失活组件调用 beforeRouteLeave

  3. 全局 beforeEach 守卫

  4. 重用组件调用 beforeRouteUpdate

  5. 路由独享守卫 beforeEnter

  6. 解析异步路由组件

  7. 激活组件调用 beforeRouteEnter

  8. 全局 beforeResolve(resolve)

  9. 导航确认

  10. 全局 afterEach 钩子

  11. DOM 更新

  12. 执行 beforeRouteEnter 中的 next(vm => {...})

⚠️ 注意:beforeRouteEnter 中无法访问 this,需通过 next 回调获取组件实例。

知识点

  • 路由守卫分类:全局、路由级、组件级。

  • 异步组件加载:在守卫后解析。

  • next 回调传参:用于 beforeRouteEnter 中获取实例。

相关推荐
2022.11.7始学前端2 小时前
n8n第七节 只提醒重要的待办
前端·javascript·ui·n8n
SakuraOnTheWay2 小时前
React Grab实践 | 记一次与Cursor的有趣对话
前端·cursor
阿星AI工作室2 小时前
gemini3手势互动圣诞树保姆级教程来了!附提示词
前端·人工智能
徐小夕2 小时前
知识库创业复盘:从闭源到开源,这3个教训价值百万
前端·javascript·github
xhxxx2 小时前
函数执行完就销毁?那闭包里的变量凭什么活下来!—— 深入 JS 内存模型
前端·javascript·ecmascript 6
StarkCoder2 小时前
求求你试试 DiffableDataSource!别再手算 indexPath 了(否则迟早崩)
前端
fxshy2 小时前
Cursor 前端Global Cursor Rules
前端·cursor
红彤彤2 小时前
前端接入sse(EventSource)(@fortaine/fetch-event-source)
前端
L、2182 小时前
统一日志与埋点系统:在 Flutter + OpenHarmony 混合架构中实现全链路可观测性
javascript·华为·智能手机·electron·harmonyos