第一轮提问(热身探测):
"Vue 是一个以响应式为核心的框架。请你用精炼的语言概括:Vue2 和 Vue3 的响应式原理核心分别是什么?并从架构或性能角度谈谈,尤雨溪为什么要在 Vue3 中换掉底层 API?换掉之后带来了哪些不可忽视的收益?"
请先自己想一想,再往下看参考答案。
参考答案
一、核心原理:同是"拦截",API 不同
Vue2 和 Vue3 的响应式核心思路是一致的:通过拦截数据的读取(get)和设置(set),自动追踪依赖、触发更新。
区别在于底层 API 的选择:
| Vue2 | Vue3 | |
|---|---|---|
| 底层 API | Object.defineProperty | Proxy |
| 拦截粒度 | 属性级别 | 对象级别 |
| 初始化时机 | 递归遍历所有属性(急切) | 访问时才代理(懒代理) |
| 支持动态属性 | ❌(需要 $set) | ✅ |
| 支持数组下标 | ❌(重写原型方法替代) | ✅ |
二、懒代理:Vue3 最核心的性能突破
Vue3 Proxy 的"懒代理"特性值得深说。
Object.defineProperty 有个无法回避的问题:它必须在初始化时就递归遍历 data 里的每一个属性,把所有嵌套对象全部劫持一遍。一个大型表单页面可能有几十个嵌套对象,全部在启动时处理,初始化性能损耗极大。
Vue3 的 Proxy 不同。reactive({ a: { b: { c: 1 } } }) 这段代码执行完之后,内部的 a、b、c 并没有立即被代理。只有当你真正去访问 state.a 时,Proxy 的 get 拦截器才会被触发,才会在那一刻对 a 的值做判断,如果它是一个对象,再递归地 reactive 包裹它。
没用到的数据,永远不会产生代理开销。这是 Vue3 响应式性能提升的核心逻辑之一。
三、架构级收益:响应式模块解耦(很多人漏掉的点)
这个点是很多同学答这道题时容易忽略的升维视角:
Vue3 把响应式系统从框架本体中完全抽离,单独发布为一个独立的 npm 包:@vue/reactivity。
这意味着什么?你可以完全脱离 Vue 框架,在任何 JS 环境(Node.js 脚本、其他框架、甚至原生项目)里直接 import { reactive, ref, computed, effect } from '@vue/reactivity' 来使用这套响应式系统。这在 Vue2 时代是做不到的——defineProperty 的响应式逻辑和框架代码是深度耦合的。
这既是 Vue3 的工程化飞跃,也是尤雨溪在架构上有意追求"关注点分离"的体现。
四、经典坑:Vue2 为什么不直接拦截数组下标?
面试时经常会被追问这个问题,也是很多人回答不清楚的地方。
很多同学的答案是:"Vue2 不支持拦截数组下标"——这其实是不准确的。
准确答案是:Object.defineProperty 在技术上完全可以拦截数组的下标访问(下标本质上就是 '0'、'1' 这样的字符串属性)。
Vue2 之所以没这么做,是纯粹出于性能考量:
一个现实的数组可能有几千个元素,如果用 defineProperty 给每个下标都添加 getter/setter,性能损耗是极其恐怖的。尤雨溪权衡之后,选择了一个更轻量的方案——重写数组的 7 个原型方法(push、pop、shift、unshift、splice、sort、reverse),只在这些方法被调用时触发响应式通知,牺牲了少量场景的响应性(直接赋值给下标不触发),换取了绝大多数场景下的性能稳定。
复盘:一个记忆锚点
如果你想在面试中把这道题说得有层次,可以用这个结构:
- 核心相同:都是"拦截 get/set,依赖收集 + 派发更新"
- API 不同:
defineProperty(属性级)→Proxy(对象级) - 性能差异:急切递归 → 懒代理,初始化开销天壤之别
- 架构飞跃:响应式模块解耦,
@vue/reactivity独立发布 - 数组处理:不是"不能"拦截,是"不该"拦截(性能考量 → 重写原型方法)
能把第 4 点和第 5 点说出来,这道题就从"及格"升到了"加分"。
下一篇:[[响应式原理02]:Proxy 懒代理如何实现深层嵌套响应式?WeakMap 又在其中扮演了什么角色?]