2784 字
14 分钟
vue
2026-02-19
2026-02-19
统计加载中...

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)

  • 代码按 datamethodscomputed 分块。
  • 问题:当组件逻辑复杂(如一个组件处理 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 是模块化的。如果你没用到 watchTransition,打包工具会自动剔除这些代码,让首屏包体积更小。

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 个变异方法(pushpopshiftunshiftsplicesortreverse)。
  • 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)来决定是否重新计算。

核心流程

  1. 懒计算 (Lazy):定义计算属性时不会立即执行。内部创建 ComputedRefImpl 实例,维护 _dirty,初始为 true
  2. 依赖收集:只有当你读取计算属性的值时,才检查 _dirty。若为 true,运行计算属性的回调函数,并在此期间进行依赖收集。
  3. 缓存机制:计算完成后 _dirty 变为 false。只要依赖项没变,后续多次读取都会直接返回缓存的 _value,不会重新运行。
  4. 调度更新:当依赖数据变化时,计算属性不会立即重新计算,而是将 _dirty 设为 true,并通知依赖该计算属性的视图更新。

为什么计算属性不能有副作用?

因为计算属性的执行时机不可预测(取决于何时被读取),所以必须是纯函数。

3.2 Ref vs Reactive#

在 Vue 3 中,两者都是创建响应式数据的工具,但底层和适用场景不同:

  • 为什么会有 ref? Proxy 只能代理「对象」,无法代理「原始值」(如数字 0)。为了让原始值也能响应式,Vue 把它包装成 { value: 0 },通过拦截这个对象的 value 属性来实现追踪。

4 组件通信#

父子组件、兄弟组件之间有哪些通信途径?

  • props / emit
  • Refs
  • v-modelmodelValue / update:modelValue
  • provide / inject
  • Vuex / Pinia
  • 状态提升

5 路由#

Vue Router 有两种模式:HashHistory。各自的特征、优缺点、适用场景与底层原理如下。

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 模式一定要后端配置?#

  1. 用户访问 example.com/,服务器返回 index.html,JS 加载,Vue 路由接管。
  2. 用户点击链接跳到 example.com/about,因 pushState,地址变了但没刷新,Vue 渲染 About 组件。
  3. 用户按 F5 刷新:浏览器请求服务器上的 /about
  4. 服务器若未配置「回退到 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 是只读的。

传统做法需要:

  1. 接收props: ['modelValue']
  2. 监听:内部监听 input 等事件
  3. 发送emit('update:modelValue', newValue)
  4. 同步:若内部要对值做处理(如格式化),可能还需在子组件里用 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:modelValue
model.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:name
const 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.memouseMemo 等做优化,属于「粗粒度」控制。

Vue:点对点的「推送」(推送式 Push-based)

  • 基于 Proxy 的响应式,在初始化时已建立依赖收集。
  • 某数据变化时,Vue 明确知道绑定在哪个组件的哪个 DOM 上,直接通知该组件更新,无需遍历整棵组件树,也无需手动优化。
  • 结论:Vue 是细粒度更新,React 是粗粒度更新。

8.4 Vue 放弃虚拟 DOM 的打算,如何看待?#

  • 通过静态提升,将静态 DOM 完全从更新逻辑中剥离。
  • 通过编译时分析,直接生成修改 DOM 的指令。
  • v-for 等仍会通过 key 来复用 DOM 节点。