vue
1 Vue 2 vs Vue 3
Vue 2 和 Vue 3 有哪些主要差异?
1.1 响应式系统的底层重构(最核心)
Vue 2 (Object.defineProperty)
-
原理:在初始化时递归遍历
data中的所有属性,为其添加 getter 和 setter。 -
痛点:
- 无法监听对象属性的新增或删除(必须用
this.$set)。 - 无法监听数组索引赋值和
length变化。 - 性能开销:如果数据结构很深,初始化时的递归遍历会造成明显的性能损耗。
- 无法监听对象属性的新增或删除(必须用
Vue 3 (Proxy)
-
原理:直接拦截整个对象的操作(13 种拦截动作),不再需要递归预处理。
-
优势:
- 完美支持新增属性、数组下标修改。
- 惰性监听:只有当你真正访问到深层属性时,才会对其进行 Proxy 包装,极大提升了初始化速度。
1.2 逻辑组织:Options API vs Composition API
Vue 2 (Options API)
- 代码按
data、methods、computed分块。 - 问题:当组件逻辑复杂(如一个组件处理 5 个功能)时,同一个功能的代码会散落在不同的块里,维护时需要不停上下滚动。Mixin 虽能复用逻辑,但存在「命名冲突」和「数据来源不明」的问题。
Vue 3 (Composition API)
- 代码按功能逻辑组织。你可以把相关的数据和方法写在一起,甚至抽离成独立的
useHooks。 - 优势:逻辑复用极其清晰,类型推导(TypeScript)支持近乎完美。
1.3 性能与体积(Compiler 优化)
- Diff 算法优化:Vue 3 引入了 Patch Flag(静态标记)。在编译阶段,它能识别出哪些 DOM 是动态的(比如带
{{}}),哪些是永远不变的静态 HTML。Diff 过程只会去对比那些有标记的动态节点,跳过整个静态树。 - 静态提升 (Hoisting):静态节点会被提升到渲染函数之外,避免每次渲染都重新创建 VNode 对象。
- Tree-shaking:Vue 3 是模块化的。如果你没用到
watch或Transition,打包工具会自动剔除这些代码,让首屏包体积更小。
1.4 碎片化特性的改进
(内容可在此补充)
2 响应式原理
2.1 从「属性拦截」到「全局代理」
Vue 2:Object.defineProperty
Vue 2 使用「数据劫持」结合「发布者-订阅者」模式。
- 过程:在组件初始化时,Vue 会遍历
data中的所有属性,利用Object.defineProperty将它们转为 getter/setter。 - 依赖收集:每个组件实例都有一个 Watcher 实例。当触发 getter 时,进行依赖收集(把 Watcher 存入 Dep);当触发 setter 时,通知 Dep 中的所有 Watcher 进行更新。
Vue 3:Proxy
Vue 3 不再修改属性本身,而是给整个对象套上一个「代理层」。
- 过程:使用 ES6 的
Proxy拦截对象的操作。通过一个全局的WeakMap来维护数据与副作用函数(Effect)之间的关系。
2.2 为什么 Vue 3 选择了 Proxy?
1. 弥补 defineProperty 的先天缺陷
- 数组索引赋值(如
arr[0] = 1)和修改长度(arr.length = 0)无法触发视图更新,在 Vue 2 中是经典痛点。 - 底层原因:
Object.defineProperty只能劫持对象的属性。虽然理论上可以遍历数组下标来劫持,但对于大量元素的数组,性能损耗不可接受。因此 Vue 2 选择放弃监听下标,转而重写数组的 7 个变异方法(push、pop、shift、unshift、splice、sort、reverse)。 - Proxy 的解决:Proxy 拦截的是整个对象,能感知数组下标的改动以及
length的变化,实现「全自动」监听。
2. 性能与扩展性
- 非侵入性:
defineProperty必须深层遍历对象并修改属性;Proxy 是声明式的代理,不需要修改原对象。 - 惰性监听:Vue 2 初始化时必须递归到底;Vue 3 只有在你访问到深层对象时,才会动态地为该层创建 Proxy,大型项目初始化速度大幅提升。
2.3 依赖收集:Watcher 与 Effect
自动收集的机制与时机
- 收集时机:发生在 Getter 阶段。当渲染函数或
watchEffect执行时,会读取响应式数据,触发 getter,从而将当前的「副作用函数」(Effect/Watcher)记录下来。 - 动态更新:依赖收集在每次执行副作用时都会重新触发。例如条件渲染
v-if="flag ? a : b":flag为 true 时收集a的依赖;变为 false 时需要清除a的依赖并收集b的。若不重新收集,a变化时仍会触发无效更新。
watchEffect 的特性
- 自动追踪:不需要像
watch那样手动指定监听哪个属性。 - 立即执行:会立即运行一次以收集依赖。
- 按需更新:只要内部用到的任何响应式数据发生变化,就会重新运行。
3 Computed 与 Watch
3.1 Computed 的底层实现原理
computed 的核心价值在于缓存。实现可概括为:它是一个特殊的副作用(Effect),通过「脏检查」开关(Dirty Flag)来决定是否重新计算。
核心流程
- 懒计算 (Lazy):定义计算属性时不会立即执行。内部创建
ComputedRefImpl实例,维护_dirty,初始为true。 - 依赖收集:只有当你读取计算属性的值时,才检查
_dirty。若为true,运行计算属性的回调函数,并在此期间进行依赖收集。 - 缓存机制:计算完成后
_dirty变为false。只要依赖项没变,后续多次读取都会直接返回缓存的_value,不会重新运行。 - 调度更新:当依赖数据变化时,计算属性不会立即重新计算,而是将
_dirty设为true,并通知依赖该计算属性的视图更新。
为什么计算属性不能有副作用?
因为计算属性的执行时机不可预测(取决于何时被读取),所以必须是纯函数。
3.2 Ref vs Reactive
在 Vue 3 中,两者都是创建响应式数据的工具,但底层和适用场景不同:
- 为什么会有 ref?
Proxy 只能代理「对象」,无法代理「原始值」(如数字
0)。为了让原始值也能响应式,Vue 把它包装成{ value: 0 },通过拦截这个对象的value属性来实现追踪。
4 组件通信
父子组件、兄弟组件之间有哪些通信途径?
- props / emit
- Refs
- v-model(
modelValue/update:modelValue) - provide / inject
- Vuex / Pinia
- 状态提升
5 路由
Vue Router 有两种模式:Hash 和 History。各自的特征、优缺点、适用场景与底层原理如下。
5.1 Hash 模式 (createWebHashHistory)
基本特征
- URL 中带有
#(例如:http://example.com/#/home)。#及其后面的内容称为 hash。
底层原理
- 不触发请求:Hash 值的变化不会被包含在 HTTP 请求中,改变
#后面的内容不会导致浏览器向服务器发送请求。 - 监听机制:浏览器原生支持
hashchange事件。Vue Router 监听该事件,根据当前 hash 匹配对应组件并渲染。
优缺点与场景
- 优点:兼容性极好;无需配置服务端,不会出现 404。
- 缺点:URL 多一个
#,不美观;SEO 较差。 - 场景:内部管理系统、对 SEO 无要求的小型应用、或无法操作后端服务器配置的情况。
5.2 History 模式 (createWebHistory)
基本特征
- URL 与普通网站一致(例如:
http://example.com/home),无#,更美观。
底层原理
- HTML5 API:使用
history.pushState()和history.replaceState()改变地址栏 URL,不触发页面刷新。 - 监听机制:监听
popstate事件处理前进、后退。
优缺点与场景
- 优点:美观;SEO 友好。
- 缺点:需要后端配合。用户在
example.com/home刷新时,浏览器会真实请求/home。若后端未配置「任意路径都返回 index.html」,会返回 404。 - 场景:现代 C 端产品、官网、对 SEO 和用户体验要求较高的项目。
5.3 为什么 History 模式一定要后端配置?
- 用户访问
example.com/,服务器返回index.html,JS 加载,Vue 路由接管。 - 用户点击链接跳到
example.com/about,因pushState,地址变了但没刷新,Vue 渲染 About 组件。 - 用户按 F5 刷新:浏览器请求服务器上的
/about。 - 服务器若未配置「回退到 index.html」,会返回 404。
解决方案:在 Nginx 或 Apache 中配置:找不到对应路径时,统一返回 index.html。
6 nextTick
如果不考虑兼容性,是否可以直接用 Promise.resolve().then() 而不用 nextTick?
- 本质原因:技术上多数情况可以,但
nextTick维护了内部队列。 - 时机问题:Vue 的渲染更新也是异步微任务。若直接用
Promise.resolve(),你的回调可能在 Vue 的渲染任务之前执行,拿到的 DOM 仍是旧的。 - 结论:
nextTick保证回调一定在 Vue 完成 DOM 更新之后执行。
7 v-model
在封装组件时,如何优雅地处理「双向绑定」?
7.1 问题
在 Vue 中数据流是单向的,父组件传给子组件的 props 是只读的。
传统做法需要:
- 接收:
props: ['modelValue'] - 监听:内部监听
input等事件 - 发送:
emit('update:modelValue', newValue) - 同步:若内部要对值做处理(如格式化),可能还需在子组件里用 watcher 或内部 ref 同步 props
这种手动同步在代码多时会很臃肿。
7.2 解决方案演进
方案 A:Computed 的 get/set(Vue 3 常规写法)
const props = defineProps(['modelValue']);const emit = defineEmits(['update:modelValue']);
const innerValue = computed({ get: () => props.modelValue, set: val => emit('update:modelValue', val),});// 子组件里直接改 innerValue.value 即可,会自动触发 emit方案 B:useVModel (VueUse)
VueUse 的 useVModel 本质是封装上述逻辑,减少模板代码。
方案 C:defineModel (Vue 3.4+)
// 子组件内部const model = defineModel();// 不需要写 defineProps 和 defineEmits// 直接修改 model.value 会自动从 props 取初值,并触发 update:modelValuemodel.value = '新值';子组件操作 v-model 就像操作本地 ref 一样简单。
7.3 多个 v-model
Vue 3 中,v-model 默认对应 prop modelValue。若使用 v-model:title:
- Prop 名为
title - 事件名为
update:title
父组件:
<UserEditor v-model:name="userName" v-model:email="userEmail" />子组件(Vue 3.4+):
<script setup>const name = defineModel('name'); // 对应 v-model:nameconst email = defineModel('email'); // 对应 v-model:email</script>
<template> <input v-model="name" placeholder="请输入姓名" /> <input v-model="email" placeholder="请输入邮箱" /></template>8 Vue 与其他框架对比
8.1 选型:Vue 与 React 如何评估?
- 团队基因:分离模式 vs All in JS
- 应用类型:React 常用于超大型 SaaS、跨端
- 招聘难度
8.2 Vue 与 React 的优缺点
(可插入对比图或表格)
8.3 Vue 相比 React,DOM 更新更精准的原因
React:自顶向下的「核查」(拉取式 Pull-based)
- 某个组件 State 变化时,React 默认会重新渲染该组件及其所有子组件。
- 即使子组件数据没变,也会被重新执行。
- 需要手动使用
React.memo、useMemo等做优化,属于「粗粒度」控制。
Vue:点对点的「推送」(推送式 Push-based)
- 基于 Proxy 的响应式,在初始化时已建立依赖收集。
- 某数据变化时,Vue 明确知道绑定在哪个组件的哪个 DOM 上,直接通知该组件更新,无需遍历整棵组件树,也无需手动优化。
- 结论:Vue 是细粒度更新,React 是粗粒度更新。
8.4 Vue 放弃虚拟 DOM 的打算,如何看待?
- 通过静态提升,将静态 DOM 完全从更新逻辑中剥离。
- 通过编译时分析,直接生成修改 DOM 的指令。
v-for等仍会通过key来复用 DOM 节点。