摘要:本文整理了 Vue 3 响应式与核心机制的常见面试要点,涵盖
toRef/toRaw的用法与绕过 Proxy 的几种方案、get/set的依赖收集与触发更新原理,并从响应式系统、代码组织方式、TypeScript 支持、模板语法行为、全局 API 五个维度对比 Vue 2 与 Vue 3 的差异;同时梳理了虚拟 DOM 与 Diff 算法、key的作用与取值建议,v-if与v-show的适用场景对比,以及var/let/const在作用域、重复声明、变量提升和可修改性上的区别。
1. toRef 常用于为响应式对象创建一个新的响应式引用,它通常用于在组件的 setup 函数中使用。
toRaw 是 Vue 3 Composition API 中的一个函数,用于将响应式引用(reactive reference)转换为普通的 JavaScript 对象,一般用于性能的优化,比如在某些情况下,你可能需要将响应式对象转换为普通对象以避免不必要的依赖追踪。针对 reactive 绕过 Proxy 的诸多问题,有以下几种解决办法:
i:用 toRef 创建响应式链接
ii:直接调用原函数的变量
iii:使用 Object.assign() 替换原来响应式的值
下面用可运行代码逐一演示toRef、toRaw以及三种绕过 Proxy 的方案。
(1)toRef:为响应式对象的某个属性创建响应式引用
import{reactive,toRef}from'vue'conststate=reactive({count:0,name:'vue'})constcountRef=toRef(state,'count')countRef.value++// 修改引用console.log(state.count)// 1,源对象同步变化state.count++console.log(countRef.value)// 2,源对象变化也会同步到引用输出:
1、2。toRef与原对象保持双向同步,且不会触发额外的依赖收集;适用于把props或reactive对象的某个属性单独传给组合式函数,避免解构后丢失响应性。
(2)toRaw:拿到原始对象,绕过 Proxy
import{reactive,toRaw}from'vue'conststate=reactive({count:0})constraw=toRaw(state)console.log(raw===state)// false,raw 是原始对象raw.count=100console.log(state.count)// 100,修改原始对象同样生效console.log(raw.count)// 100输出:
false、100、100。读取/修改raw不会触发依赖收集与更新,适用于把数据交给不需要响应式的第三方库(如 ECharts、地图 SDK)或做性能敏感的大对象处理。
(3)方案一:用 toRef 创建响应式链接
import{reactive,toRef}from'vue'conststate=reactive({user:{name:'Tom'}})constnameRef=toRef(state.user,'name')nameRef.value='Jerry'console.log(state.user.name)// Jerry输出:
Jerry。用toRef把深层属性“挂”成独立引用,既保留响应性又避免直接操作 Proxy 包装后的对象;适用于需要把某个字段单独暴露给模板或子函数的场景。
(4)方案二:直接调用原函数的变量(保存原始对象引用)
import{reactive,toRaw}from'vue'conststate=reactive({list:[1,2,3]})constrawList=toRaw(state).list// 保存原始数组引用rawList.push(4)console.log(state.list)// [1, 2, 3, 4]console.log(rawList===state.list)// false,rawList 是原始数组输出:
[1, 2, 3, 4]、false。直接持有原始变量后操作它,可避免每次读写都走 Proxy 拦截;适用于批量数据处理、循环内高频读写等对性能敏感的场景,但要注意此时不会触发视图更新。
(5)方案三:用 Object.assign() 替换原来响应式的值
import{reactive}from'vue'conststate=reactive({a:1,b:2})Object.assign(state,{a:10,c:3})console.log(state)// { a: 10, b: 2, c: 3 }输出:
{ a: 10, b: 2, c: 3 }。Object.assign通过 Proxy 的set拦截逐项赋值,新增属性也能被追踪;适用于需要整体合并/重置对象字段、又不想丢失响应性的场景。
2. get() 和 set() 是 Vue 3 Composition API 中用于访问和修改响应式对象属性的方法。
get() 方法用于获取响应式对象的属性值,只有在读取该值时才会触发依赖收集,否则不会执行。
set() 方法用于设置响应式对象的属性值,只有在写入新值时才会触发更新,否则不会执行。其中 set(newValue) 的 newValue 即为新值。
3. Vue 2 和 Vue 3 的区别:
响应式系统:
Vue 2 基于Object.defineProperty劫持对象的每个属性,因此无法监听对象新增/删除属性以及数组索引赋值,需要借助Vue.set/Vue.delete和重写数组方法来打补丁;
Vue 3 改用Proxy代理整个对象,能天然拦截属性的读取、设置、新增、删除以及数组索引修改,不再需要这些特殊 API,响应式能力更完整、行为也更符合直觉。
代码组织方式:
Vue 2 使用 Options API,把data、methods、computed、生命周期等按选项分门别类,组件逻辑复杂时同一功能的代码会被拆散到不同选项里;
Vue 3 引入 Composition API(setup/<script setup>),允许按功能把相关的ref、computed、watch和函数写在一起,逻辑内聚性更强,也更容易抽成可复用的组合式函数。
TypeScript 支持:
Vue 2 对 TypeScript 的支持比较有限,需要借助额外库且类型推断不够精准;
Vue 3 完全用 TypeScript 重写,类型定义更完善,对 TS 用户更友好,组合式函数也更容易获得类型推断。
模板与语法行为:
Vue 2 强制组件只能有一个根节点,常需要加无意义的包裹元素,且v-if与v-for同时使用时v-for优先级更高,导致v-if在每次循环中重复判断;
Vue 3 支持多根节点(Fragment),并把v-if优先级调整为高于v-for,条件不满足时连循环都不会执行,同时v-model统一了.sync的功能并支持在一个组件上绑定多个v-model。
全局 API 与应用实例:
Vue 2 通过new Vue()创建根实例,Vue.use()、Vue.component()等全局 API 会污染全局;
Vue 3 改为createApp()创建独立应用实例,全局 API 挂载在实例上,多个应用之间互不影响,隔离性更好。
4. 虚拟 DOM
- 虚拟 DOM:用 JS 对象描述 DOM 结构,避免频繁操作真实 DOM,提升渲染效率,支持跨平台渲染。
因此在创建虚拟 DOM 时,会生成对应的 JS 对象以及用于 Diff 的 VNode 结构
- Diff 算法:同层级对比新旧虚拟 DOM,找出最小更新差异,不同层级直接新建虚拟 DOM;有 key 时通过 key 映射 + 最长递增子序列优化节点移动,降低 DOM 操作;用 patch 函数来对比。
下面是 Diff 算法的完整流程图:
各步骤作用说明:
- 同层级对比:只比较同一层级的节点,不跨层级比较,把 Diff 复杂度从 O(n³) 降到 O(n),不同层级直接整棵新建。
- key 映射:用
key -> index的 Map 快速判断新旧节点是否可复用,避免遍历查找,时间复杂度为 O(1)。 - 最长递增子序列(LIS):找出旧列表中已经保持相对顺序的最长一段节点,这些节点无需移动,其余节点才执行移动,从而把 DOM 移动次数降到最少。
- patch 更新:对可复用节点递归比较 props、文本和子节点,只把真正变化的差异落到真实 DOM 上,减少重排重绘。-Diff 算法:同层级对比新旧虚拟 DOM,找出最小更新差异,不同层级直接新建虚拟 DOM;有 key 时通过 key 映射 + 最长递增子序列优化节点移动,降低 DOM 操作;用 patch 函数来对比。
- key:节点唯一标识,帮助 Diff 准确匹配新旧节点,复用 DOM 元素,防止列表更新时组件/表单状态错乱,优化对比性能,时间复杂度为 O(1);禁止用随机值,优先使用唯一 id,不推荐索引。没有 key 的时候需要遍历所有节点
5. v-if 和 v-show
| 对比项 | v-if | v-show |
|---|---|---|
| DOM行为 | false → 删除DOM;true → 创建DOM | 始终保留DOM,切换display |
| 初始渲染开销 | 条件为false时开销小(不渲染) | 无论真假,初始都会渲染DOM,开销更大 |
| 切换开销 | 频繁切换开销大(不断创建销毁DOM) | 切换开销很小,只改CSS属性 |
支持v-else / v-else-if | ✅ 支持 | ❌ 不支持 |
模板<template> | ✅ 可用(分组渲染) | ❌ 不生效 |
| 生命周期 | 切换时组件会执行mounted / unmounted | 不会触发挂载、卸载生命周期 |
✅用 v-if
- 条件很少改变,一次性判断(比如权限控制页面模块)
- 初始大概率不显示,减少首次DOM渲染
- 需要配合
v-else、<template>
✅用 v-show
- 需要频繁切换显示隐藏(弹窗、tab切换、下拉菜单)
6.let,const,var的区别
- 作用域
| 作用域 | |
|---|---|
| var | 函数作用域 |
| let | 块级作用域 |
| const | 块级作用域 |
- 重复声明,变量提升,全局挂载(挂载在windows)
都是var特有的
- 是否可修改
const不能修改