Appearance
Vue3 与 Vue2 的差异
Vue3 不是 Vue2 的"升级版",是重写。虽然 API 保持了相当的延续性,但内部从响应式系统到编译器到渲染层全部翻新了。理解这两者的差异,比记住 API 对照表重要得多。
响应式系统:从 defineProperty 到 Proxy
Vue2 的响应式基于 Object.defineProperty。这个选择在 2016 年是合理的(Proxy 还没广泛支持),但它留下的坑每个 Vue2 开发者都踩过。
Object.defineProperty 劫持的是对象的属性,不是对象本身。后果:
js
// Vue2 的响应式漏洞
const vm = new Vue({
data: { obj: { a: 1 } }
})
vm.obj.b = 2 // ❌ 不响应——b 是新属性,defineProperty 不会自动追踪
vm.arr = [1, 2, 3]
vm.arr[0] = 99 // ❌ 不响应——数组索引赋值不会触发 setter
vm.arr.length = 0 // ❌ 不响应——length 修改也不会触发Vue2 为此在 Vue.set / this.$set 上打了补丁,以及对数组的 7 个变异方法(push/pop/shift/unshift/splice/sort/reverse)做了 monkey-patch。但治标不治本——如果你用 arr[0] = x 或 arr.length = 0,该不响应还是不响应。
Vue3 换了 Proxy:
js
// Vue3 reactive() 的简化模型
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
const result = Reflect.get(target, key, receiver)
track(target, key) // 依赖收集
return isObject(result) ? reactive(result) : result // 惰性深层代理
},
set(target, key, value, receiver) {
const oldValue = target[key]
const result = Reflect.set(target, key, value, receiver)
if (oldValue !== value) {
trigger(target, key, value) // 派发更新
}
return result
},
deleteProperty(target, key) {
const hadKey = Object.prototype.hasOwnProperty.call(target, key)
const result = Reflect.deleteProperty(target, key)
if (hadKey) {
trigger(target, key, undefined) // 删除属性也触发更新
}
return result
}
})
}Proxy 劫持的是对象本身——属性增删、数组索引赋值、length 修改,全部自动追踪。$set 和变异方法补丁成为历史。
而 Reflect 的作用不止是"配合 Proxy 使用":
Reflect.get/set/deleteProperty返回布尔值表示操作是否成功,Proxy handler 需要这个返回值来决定是否抛异常。Reflect.set的receiver参数确保在原型链场景下 this 正确指向 Proxy 对象而非原始对象——这是target[key] = value做不到的。
track() 和 trigger() 是响应式系统的两个核心:
text
get 被访问 → track(target, key)
→ 查找 target 对应的 depsMap(WeakMap<Target, Map<Key, Dep>>)
→ 在 depsMap 中找到 key 对应的 dep(Set<ReactiveEffect>)
→ 将当前活跃的 effect 加入 dep
set 被调用 → trigger(target, key, newValue)
→ 找到 target → key → dep
→ 遍历 dep 中的每个 effect
→ 调用 effect.scheduler() 或 effect.run()Vue2 的依赖收集在 Object.defineProperty 的 getter/setter 里,依赖关系存储在闭包中,结构是 Dep(target, key) → [Watcher]。Vue3 的依赖图结构是 WeakMap<Target, Map<Key, Set<ReactiveEffect>>>——层级更清晰,且用 WeakMap 使得 target 在被释放时自动从 depsMap 中移除,避免内存泄漏。
另外,reactive() 是惰性深层代理——只有当你实际访问 obj.a.b 时,b 才会被包装为 reactive。Vue2 在 data() 返回时立即递归遍历整个对象树,每个属性都调用 defineProperty。对大型数据对象,Vue3 的首屏初始化更快。
ref vs reactive:适用边界
Vue3 提供了两条响应式路线:ref 和 reactive。它们的差异不是风格偏好,是底层机制使然。
reactive() 只能用 Proxy 代理对象(Object/Array/Map/Set)。基本类型(string/number/boolean)没有引用,Proxy 无从代理——所以 reactive(1) 会直接报错。
ref() 内部维护一个 .value 属性,基本类型被包裹在 { value: xxx } 中,这个包裹对象本身是 reactive 的:
js
// ref 的简化实现
function ref(value) {
const r = {
get value() {
track(r, 'value')
return value
},
set value(newVal) {
if (newVal !== value) {
value = newVal
trigger(r, 'value')
}
}
}
return r
}.value 在 <script setup> 模板中会被编译器自动解包,模板里可以直接写 count;在 JS 中仍然需要显式访问 count.value。这也是 ref 使用时最容易出现遗漏的位置。
shallowRef 和 shallowReactive 提供了更细的响应式边界控制:只有顶层属性是响应式的,内部对象不会被深层代理。对于每一项都比较复杂、但更新方式主要是整体替换而不是深层修改的大列表,shallowRef 可以省掉深层 reactive 化的递归开销。
编译器优化:Vue3 比 Vue2 快在哪
"Vue3 比 Vue2 快"通常被归因于 Proxy 替代了 defineProperty,但 Proxy 主要是修复了响应式漏洞,对性能的贡献有限。真正的性能提升来自编译器。
静态提升(Hoisted Static Nodes)
Vue2 的模板编译:每个渲染周期,整个 VNode 树重新创建,包括静态节点(从不变化的 DOM 结构)。
Vue3 编译器识别静态节点,将其创建提升到渲染函数之外:
js
// Vue2 每次渲染都创建
function render() {
return h('div', [
h('span', '我是静态文本'), // 每次都重新创建 VNode
h('span', this.dynamicText) // 动态部分
])
}
// Vue3 编译后(简化)
const _hoisted_1 = h('span', '我是静态文本') // 只创建一次
function render() {
return h('div', [
_hoisted_1, // 直接复用
h('span', this.dynamicText) // 只更新动态部分
])
}Patch Flag(动态节点标记)
Vue3 编译器为每个带动态绑定的节点生成一个数字 flag,标识"这个节点哪些部分是动态的":
js
// 模板:<div :class="cls" :id="id">text</div>
// 编译后的 VNode
{
type: 'div',
props: { class: ctx.cls, id: ctx.id },
children: 'text',
patchFlag: 1 | 16 // 1 = 动态文本, 16 = 动态 props
}运行时 diff 时,Vue3 先看 patchFlag。如果 flag = -1(静态节点),跳过 diff 直接复用。比 Vue2 全量比较 children props 省了大量无意义工作。
Block Tree(靶向 diff)
这是 Vue3 编译器最巧妙的设计。编译器将模板分割为 block——每个 block 是一个"动态节点集合"。Block 内部只 diff 动态节点,跳过所有静态内容。
html
<template> <!-- block tree -->
<div> <!-- root block -->
<span>静态</span> ← 跳过 diff
<span>{{ dynamic }}</span> ← patchFlag 标记
<div v-if="show"> <!-- 嵌套 block -->
<p>静态</p> ← 跳过
<p :id="id">动态</p> ← patchFlag 标记
</div>
</div>
</template>Vue2 的 diff 是全量 children 遍历——哪怕 90% 的节点是静态的,每个都会被遍历一次。Vue3 跳过全部静态节点,只 diff 带有 patchFlag 的动态节点。实际项目中的 diff 量减少了 60-80%。
Composition API 不是"另一种写法"
把 Composition API 理解为"和 Options API 功能等价但写法不同"是对它最大的误解。
Options API 的代码组织方式是按选项类型(data/methods/computed/watch)切分的。在一个复杂组件中(500+ 行),同一个业务逻辑的代码分散在 data、methods 和 watch 之间——读代码时需要在几个选项块之间跳来跳去。
Composition API 按逻辑关注点组织:
js
// Options API:同一功能分散在多个选项中
{
data() {
return {
searchQuery: '',
searchResults: [],
searchLoading: false
}
},
watch: {
searchQuery: {
handler(q) { this.doSearch(q) },
immediate: true
}
},
methods: {
async doSearch(q) { /* ... */ }
}
}js
// Composition API:搜索功能全部集中在一个 composable 中
function useSearch() {
const query = ref('')
const results = ref([])
const loading = ref(false)
watch(query, async (q) => {
loading.value = true
results.value = await fetchSearchResults(q)
loading.value = false
})
return { query, results, loading }
}
// 在组件中使用
const { query, results, loading } = useSearch()这带来的收益不是单纯的“代码更干净”,而是逻辑的单位化。useSearch() 可以通过显式函数调用在任意组件中复用,而不是依赖 mixin。相比之下,mixin 更容易引入命名冲突、来源不透明和隐含依赖。Composable 的输入、调用位置和返回值都更直观。
Composition API 也显著改善了 TypeScript 的可用性。Options API 中的 this 类型推断容易受到 mixin、插件和原型扩展影响;Composition API 以普通函数调用和变量声明为主,更适合静态分析。
生命周期和组件实例
Vue3 引入了 setup() 作为组件入口。它的执行时机在 beforeCreate 和 created 之间——也就是说,执行 setup() 时组件实例已经创建了,但还没有开始执行 Options API 的 data() 和 computed。
所以在 setup() 中没有 this——不是故意不给,是 setup() 执行时组件实例还没完全初始化。需要用到的数据(props、context)通过 setup(props, { attrs, slots, emit }) 参数传入。
生命周期钩子的对应关系:
| Vue2 Options API | Vue3 Composition API | 变化说明 |
|---|---|---|
| beforeCreate | —(setup 替代) | setup() 在此时机执行 |
| created | —(setup 替代) | setup() 在此时机执行 |
| beforeMount | onBeforeMount | 命名统一为 onXxx |
| mounted | onMounted | |
| beforeUpdate | onBeforeUpdate | |
| updated | onUpdated | |
| beforeDestroy | onBeforeUnmount | 语义从"销毁"改为"卸载" |
| destroyed | onUnmounted | |
| errorCaptured | onErrorCaptured | 新增 |
| — | onRenderTracked | Vue3 新增:调试用 |
| — | onRenderTriggered | Vue3 新增:调试用 |
beforeDestroy/destroyed 改名为 beforeUnmount/unmounted 的原因:Vue3 的组件实例生命周期不只是"销毁",还有 keep-alive 的缓存/激活和 <Teleport> 的挂载/卸载。unmount 比 destroy 更准确地描述了"从 DOM 中移除组件"这一行为。
其他关键差异
Fragment(多根节点)
Vue2 的组件模板必须有唯一根元素,因为 Vue2 的 VNode 树假设每个组件对应一个根节点。
Vue3 支持多根节点(Fragment),编译器在内部用一个不可见的 Fragment 节点包裹多个根,渲染时将 Fragment 的 children 展平插入 DOM。这消除了 Vue2 中大量无意义的包裹 <div>。
Teleport
Vue2 中要将内容渲染到组件树之外(比如 modal 渲染到 <body> 下),需要手动调 document.body.appendChild——这脱离了 Vue 的虚拟 DOM 管理,后续更新需要手动同步。
Vue3 的 <Teleport to="body"> 在虚拟 DOM 层面保持父组件的响应式连接(props、事件、provide/inject 正常工作),但在 DOM 层面将内容挂载到指定位置。Modal、Tooltip、Notification 这些场景不再需要 hack。
Suspense
Vue3 的 <Suspense> 提供了异步组件的加载状态管理——在异步依赖(async setup、异步组件)resolve 之前显示 fallback 内容。Vue2 需要用 v-if + loading state 手动管理。
但 <Suspense> 到 Vue3.5 为止仍然是实验性功能(在 Vue 3.3+ 中标记为 stable,但生产环境的广泛使用还需要时间验证)。
Tree-shaking
Vue2 的全局 API 挂在 Vue 构造函数上:Vue.nextTick()、Vue.set()、Vue.component()。打包时即使项目里没用 Vue.set,它仍然会被打包进去。
Vue3 的 API 全部按命名导出:import { ref, computed, watch } from 'vue'。只打包你实际 import 的。一个典型的 Vue3 SPA 的 Vue 运行时(不包含第三方库)可以压缩到 16KB gzipped,Vue2 是 23KB+——差异来自 tree-shaking 和编译器输出的更高效代码。
TypeScript 支持
Vue2 的 TypeScript 支持是通过 vue-class-component 和 vue-property-decorator 两个装饰器库实现的——这两个库不是官方维护的,类型推导有各种边界情况失效。
Vue3 本身用 TypeScript 重写,类型推导在 Composition API 中是原生的。defineProps<>()、defineEmits<>()、ref<>() 的类型参数让你可以在不写额外类型标注的情况下获得完整的 props/slots/emit 类型安全。
Vue2 到 Vue3:Breaking Changes 速查
迁移时最容易踩的 5 个 breaking changes:
v-model行为变更:Vue2 中v-model是valueprop +input事件的语法糖。Vue3 改为modelValueprop +update:modelValue事件。同时支持多个v-model:title、v-model:content。v-if/v-for优先级反转:Vue2 中v-for优先级 >v-if;Vue3 中v-if>v-for。Vue2 的做法导致每次渲染都遍历整个列表然后过滤;Vue3 的做法更符合直觉但会导致依赖两者的代码行为变化。- 事件总线(Event Bus)被移除:Vue2 的
new Vue()可以当 event bus 用($on/$off/$emit)。Vue3 移除了$on/$off/$once。替代方案:mitt(轻量 event emitter)或使用 provide/inject + callback。 - filters 过滤器被移除:Vue2 的
0语法在 Vue3 中不再支持,可改为 computed 或普通方法。 $listeners被合并到$attrs:Vue2 中$attrs只包含非 prop、非 class/style 的属性,$listeners单独包含事件。Vue3 合并两者——$attrs包含所有非 props 属性(包括onXxx事件处理器)。
Vue 官方提供了 @vue/compat 迁移构建——行为兼容 Vue2,但会在控制台输出废弃 API 的警告。你可以先把项目升级到 compat build,在控制台逐个修复警告,确认所有警告消除后切换到纯 Vue3 build。渐进式迁移,而不是大爆炸重写。
性能基准:数字说话
Bundle Size
| 指标 | Vue 2.7 | Vue 3.5 | 差异 |
|---|---|---|---|
| 运行时(gzipped) | ~23KB | ~16KB | -30% |
| 运行时 + 编译器(gzipped) | ~32KB | ~22KB | -31% |
| Tree-shakable | 否 | 是 | 按需打包 |
差异来源:Vue3 的编译器输出更高效的代码(静态提升、patch flag 减少了运行时所需的代码路径);全局 API 从挂载变为命名导出使 tree-shaking 生效;以及内部模块的代码体积优化。
渲染性能
基于 js-framework-benchmark(Chrome 132,2026 Q1):
| 场景 | Vue 2 | Vue 3 | 提升 |
|---|---|---|---|
| 创建 1K 行 | ~55ms | ~25ms | 2.2x |
| 更新 1K 行(全部替换) | ~45ms | ~18ms | 2.5x |
| 部分更新(1 行 / 1K) | ~350μs | ~80μs | 4.4x |
| 选择行(选中高亮) | ~28ms | ~12ms | 2.3x |
| 内存(1K 行) | ~22MB | ~14MB | -36% |
部分更新的差距(4.4x)最大——这正是编译器优化的主战场。Vue2 需要全量 diff children 来找到变化的那一行;Vue3 通过 patchFlag 和 block tree 直接定位到动态节点,跳过所有静态内容。
Scheduler 与浏览器渲染管线的交互
Vue3 的 scheduler 在浏览器事件循环中的工作方式,是理解"为什么 Vue3 比 Vue2 渲染更高效"的关键:
text
用户操作 → 同步修改响应式数据
→ trigger() 将 effect 加入 scheduler queue
→ 同一个 tick 内多次修改被去重(同一个 effect 只入队一次)
→ nextTick(微任务)flush queue
→ 批量执行所有 effect → 更新 DOM
→ 浏览器在下一帧进行 layout/paint/compositeVue2 的更新也是批量的(通过 nextTick),但 scheduler 没有优先级概念。Vue3 的 scheduler 支持 queueJob 和 queuePostFlushCb 两级队列——前者在 DOM 更新前执行,后者在 DOM 更新后执行。这允许你在正确的时机做正确的事(比如 watchPostEffect 可以在 DOM 更新后操作更新的节点)。
另外,Vue3 的 nextTick 返回一个 Promise(Vue2 的 Vue.nextTick 接受回调)。这意味着可以直接 await nextTick() 而不是嵌套回调,在需要等待 DOM 更新后才做操作时(比如测试中验证更新后的 DOM 结构)代码更清晰。
架构视角:Vue3 的设计为什么更适合大型应用
Vue2 的架构是扁平的——全局 API 挂在一个构造函数上,mixin 是唯一复用逻辑的机制,依赖注入(provide/inject)是后来补的,TypeScript 支持是第三方库拼凑的。
Vue3 的架构是多层的:
text
应用层
├── SFC Compiler(<script setup> + 模板编译)
│ └── 编译时优化:静态提升 / patchFlag / block tree
├── Runtime Core(@vue/runtime-core)
│ ├── Reactivity System(@vue/reactivity,可独立使用)
│ │ ├── reactive / ref / computed / watch
│ │ └── track / trigger / effect 调度
│ ├── Virtual DOM Renderer
│ │ ├── 组件实例生命周期
│ │ ├── diff 算法(靶向更新)
│ │ └── scheduler(queueJob / queuePostFlushCb)
│ └── Composition API(setup / composables)
└── Platform Adapter(@vue/runtime-dom / @vue/runtime-test)每一层的职责边界清晰。@vue/reactivity 可以完全脱离 Vue 组件系统独立使用——你可以只用 Vue 的响应式系统来管理应用状态,而用别的渲染方案(甚至是 Canvas/Three.js)。
这种模块化设计带来的副产品:
- 跨平台:
@vue/runtime-core是平台无关的。@vue/runtime-dom适配浏览器,@vue/runtime-test用于单元测试。社区已经基于 runtime-core 构建了 Weex、小程序等多种渲染目标。 - 渐进式采用:可以在现有的 jQuery/Bootstrap 项目里引入
@vue/reactivity做状态管理,逐步迁移到 Vue 组件。 - Tree-shaking:模块化使 tree-shaking 生效。只 import 你使用的 API。
Composition API 对架构的影响
Vue2 的 mixin 是横向复用——多个 mixin 注入到同一个组件中,每个 mixin 都向组件的 data/methods/computed 注入内容。组件越大、mixin 越多,数据来源越不可追踪。
Composition API 改变了复用模型——从"注入"变成"组合":
text
Vue2 mixin 模型:
Component
├── mixin A (data.x + method.doA)
├── mixin B (data.y + method.doB)
└── mixin C (data.z + method.doC)
→ data 中的 x/y/z 来源不透明,命名冲突难以排查
Vue3 composable 模型:
const { x, doA } = useFeatureA()
const { y, doB } = useFeatureB()
const { z, doC } = useFeatureC()
→ 每个变量和函数的来源完全可追踪在大规模应用中(50+ 个组件、10+ 个开发者),composable 的可追踪性比 mixin 的黑盒注入在维护成本上有数量级的差异。
生产实践:Vue2 → Vue3 迁移的真实经验
迁移决策:不是所有项目都需要迁移
Vue2 于 2023 年 12 月 31 日 EOL(End of Life)。但这不意味着 2024 年 1 月 1 日所有 Vue2 项目必须迁移。核心判断:
- 项目仍在活跃迭代 → 迁移。新功能在 Vue3 上开发,Vue2 上只修 bug。
- 项目进入维护模式(只修 bug、不新增功能) → 不迁移。Vue2 虽然 EOL 但没有安全补丁的风险对纯前端代码不构成威胁(前端运行在浏览器沙箱中,没有 RCE 风险)。
- 项目将在 6-12 个月内下线或重写 → 不迁移。迁移成本无法在剩余生命周期内收回。
- 项目使用的 Vue2 生态库没有 Vue3 版本 → 延迟迁移。检查关键依赖(UI 框架、图表库、富文本编辑器)是否有 Vue3 兼容版本。vue-demi 可以帮助库作者同时支持 Vue2 和 Vue3,但不能解决所有问题。
渐进式迁移策略
完全遵循 Strangler Fig 模式:
text
阶段 1: 安装 @vue/compat(1 天)
→ 项目正常运行,控制台出现废弃 API 警告
→ 逐个消除警告(通常 1-2 周)
阶段 2: 切换到纯 Vue3 build(1 天)
→ 移除 @vue/compat
→ 确保所有测试通过
阶段 3: 逐个组件迁移到 Composition API(按需,逐步)
→ Options API 在 Vue3 中仍然完全可用
→ 仅在新功能或需要复用的逻辑上使用 Composition API
→ 不必"一次性全部重构"关键实践:
- 先升级构建工具:Vue3 需要 Vite 或 Webpack 5。如果还在用 Vue CLI(基于 Webpack 4),升级构建工具是迁移的第一步。
- UI 框架先于业务代码:Element UI → Element Plus,Ant Design Vue 1 → 3/4。UI 框架的 API 变化通常比 Vue 核心更大。
- 测试先行:在迁移前确保测试覆盖率 ≥70%。没有测试的迁移是盲飞——你只能靠手动点击发现回归 bug。
- 双版本并行期不要超过 3 个月:如果团队分两组分别维护 Vue2 和 Vue3 版本超过 3 个月,同步成本会超过迁移成本。
典型坑
$attrs和$listeners合并导致的事件穿透:Vue3 中$attrs包含所有非 props 属性,包括onClick等事件处理器。如果你以前手动传递$attrs和$listeners到子组件,Vue3 中只需传$attrs。但如果你的组件以前依赖"事件不被自动传递"这个行为(比如通过检查$listeners来决定是否传播事件),行为会发生变化。v-model从单通道变成多通道:Vue2 中v-model只能绑定一个值。如果代码依赖.syncmodifier(Vue2 中给子组件传:value.sync),Vue3 中需要改成v-model:value。transition组件的 class 名称变更:Vue3 中 CSS transition class 从v-enter改为v-enter-from。如果项目中有自定义 transition CSS,需要逐一更新。
几个 Principal 级别的判断
1. Vue2 → Vue3 的真实价值不在性能,在可维护性。
2x 的渲染性能提升和 30% 的 bundle size 缩减——这些数字在 benchmark 里漂亮,但在实际项目中用户几乎感知不到。真正改变团队日常开发的,是 Composition API 的逻辑组织能力、TypeScript 的类型安全、和编译器的错误提示(<script setup> 的编译时检查和更直观的报错信息)。这些不能用数字量化,但 6 个月后会体现在 bug 率和开发速度上。
2. "Vue3 的 Composition API"和"Vue2 的 Options API"不是对立关系。
Vue3 中 Options API 仍然完全可用,可以和 Composition API 混用。不需要把现有的 Options API 代码重写成 Composition API——除非你要复用某段逻辑。正确的迁移心态不是"换成新的写法",而是"在新的地方用新的写法"。渐进式升级的价值在于不用一次性重写。
3. Proxy 解决了响应式漏洞,但也引入了新的心智负担。
reactive() 的 Proxy 对象和原始对象的关联、解构丢失响应性、ref 的 .value 在 JS 和模板中的不同行为——这些是 Vue3 使用者最常遇到的概念难点。Vue2 的 Object.defineProperty 虽然有漏洞,但概念模型简单("data 里的东西就是响应式的")。选 Vue3 不只是选了更好的技术,也是选了更陡峭的学习曲线。
4. @vue/reactivity 的独立性是 Vue3 被低估的特性。
可以脱离 Vue 组件体系,在纯 Node.js 脚本、CLI 工具、甚至后端服务中使用 Vue 的响应式系统。这对于需要复杂状态管理的场景是一个轻量替代方案——不需要引入整个 Vue 框架。许多项目用 @vue/reactivity 来管理 WebSocket 推送的数据流、处理复杂表单的联动逻辑。
5. 不要因为"Vue3 是最新的"就迁移。
如果 Vue2 项目运行稳定、没有新功能计划、没有安全补丁需求、没有 Composition API 或 TypeScript 的强烈需求——不迁移是完全合理的选择。技术栈的稳定性本身就是价值。迁移的理由是"目前的约束(响应式漏洞、TypeScript 缺失、mixin 维护困难)在伤害业务",不是"Vue3 更新"。
扩展问题
Q1: Vue3 的 reactive() 有什么局限性?什么情况下你会选 ref 而不是 reactive?
三个核心局限:
reactive()只能代理对象类型(Object/Array/Map/Set),基本类型无法使用。reactive()返回的 Proxy 对象不能被解构——const { x, y } = reactive({ x: 1, y: 2 })之后x和y都是普通值,不再响应式。需要用toRefs()转换。reactive()的 Proxy 对象不能直接被替换——let state = reactive({}); state = reactive({})是创建了两个独立的响应式对象,原来引用state的地方不会自动更新到新对象。
所以通常建议:局部状态用 ref,复杂对象用 reactive。ref 的 .value 虽然麻烦但行为可预测——解构、替换、传递都不会丢失响应性。reactive 更适合"这是一个整体对象,我只通过属性访问它"的场景。
Q2: Vue3 的 diff 算法在哪些场景下不如 Vue2?
Vue3 的靶向 diff 在动态节点占比极高的场景下没有优势——如果模板中 > 80% 的节点都是动态的,patchFlag 和 block tree 的跳过优化基本失效。另外,Vue3 的 block tree 对 v-for 内部的动态节点追踪深度有限——如果 v-for 内的每个 item 都有多层嵌套的动态节点,block 的开销可能超过收益。
但这些场景在实际项目中罕见——通常情况下静态节点的比例远大于动态节点。
Q3: watchEffect 和 watch 的区别只是"自动追踪依赖"吗?更深层的差异是什么?
除了依赖追踪方式的差异(watchEffect 自动追踪 vs watch 显式指定),更深层的差异在于执行时机和旧值的可用性:
watchEffect在创建时立即同步执行一次,之后在依赖变化时异步执行(加入queuePreFlushCb)。旧值不可用——callback 只接收副作用清理函数,不接收新旧值对比。watch默认是懒执行的(除非传immediate: true),依赖变化后的执行默认也是异步的(但可以用flush: 'sync'强制同步)。提供了新旧值对比。watchEffect更适合"我需要每当这些依赖变化时做一件事,不关心之前是什么值"(比如同步数据到 localStorage、更新 document title)。watch更适合"我需要知道从什么值变成了什么值"(比如表单脏检测、撤销/重做栈)。
Q4: Vue3 的 nextTick 和 React 的 useEffect 在执行时机上有什么不同?
nextTick 是在当前 tick 的同步代码执行完毕后、浏览器进行下一帧渲染之前执行的微任务。它的执行顺序是:同步代码 → nextTick 回调 → 浏览器渲染。
useEffect 是在浏览器完成渲染(layout + paint)之后异步执行的。执行顺序是:同步代码 → 浏览器渲染 → useEffect 回调。
这意味着在 nextTick 中可以同步访问更新后的 DOM(此时 DOM 已经更新但可能还没渲染到屏幕),在 useEffect 中则是在用户看到新 UI 之后才执行。Vue 的 nextTick 更适合"更新之后立即操作 DOM"的场景(比如测量元素尺寸、设置焦点),React 的 useEffect 更适合"不阻塞渲染的副作用"(比如数据上报、分析埋点)。
Q5: 如果你要说服团队从 Vue2 迁移到 Vue3,你会强调哪三个论点?你又会主动提及哪三个风险?
强调的论点:
- Composition API 的逻辑复用能力——mixin 在 10+ 个组件中复用时的命名冲突和来源不透明问题在 Composables 中彻底消失。
- TypeScript 的原生支持——不需要装饰器库和类型体操,类型推导开箱即用。对于中大型项目,这意味着更少的运行时类型错误。
- 生态的未来方向——新库和新功能默认基于 Vue3,Vue2 的生态已经进入停滞(Vuetify 2、Element UI 等库不再更新)。
主动提及的风险:
- 学习曲线——Composition API 的
.value心智模型、refvsreactive的选择、生命周期钩子的变化,团队需要 2-3 周适应期。 - 第三方依赖兼容性——部分小众但关键的依赖可能没有 Vue3 版本,需要自行寻找替代或 fork 维护。
- 迁移期间的业务停滞——如果迁移需要 3 个月,这 3 个月内原计划的业务功能会被推迟。需要和管理层对齐"迁移的投资回报率"。
