真实面经题目 · 原创解析

说说 Vue 的响应式原理吧

这题考察 Vue 响应式如何把数据读写、依赖收集和视图更新串起来,回答时要以 Vue2 的 defineProperty 为主,并能说明 Vue3 Proxy 解决了哪些边界。

出现于:携程 · 前端

60 秒回答模板

Vue 响应式的核心是把数据读取和数据修改变成可追踪的行为。Vue2 在初始化时递归遍历 data,用 Object.defineProperty 给属性加 getter/setter;组件渲染或 computed/watch 读取数据时,当前 watcher 会被 getter 收集到 dep 中;数据被 set 时,dep.notify 通知 watcher,watcher 再进入队列,异步批量更新视图。Vue2 对新增属性、删除属性、数组下标和 length 修改不够自然,所以需要 Vue.set、数组变异方法劫持等补丁。Vue3 用 Proxy 代理整个对象,能拦截 get、set、deleteProperty、has、ownKeys,也能更好支持 Map、Set 等集合类型,但 reactive/ref 的使用边界仍要分清。

考点 核心机制与工程取舍
难度 中高频面试题
回答目标 按定义、机制、场景讲清楚

深入解析

01

先讲响应式目标

响应式不是单纯监听变量变化,而是建立 data -> watcher -> render 的依赖关系。组件渲染读取了哪些字段,这些字段变化时才需要通知对应组件或计算属性更新。

02

Vue2 初始化怎么劫持

Vue2 会对 data 做 observe,给每个对象属性通过 Object.defineProperty 定义 getter 和 setter。对象值会继续递归 observe,数组会替换原型上的 push、pop、splice 等变异方法来触发通知。

03

依赖收集发生在读取阶段

渲染 watcher、computed watcher 或用户 watcher 执行时会成为当前 active watcher。getter 被触发后,通过 dep.depend 把当前 watcher 收集起来,同一个字段可以对应多个 watcher,同一个 watcher 也可能依赖多个字段。

04

派发更新发生在写入阶段

setter 比较新旧值后会重新 observe 新值,并通过 dep.notify 通知 watcher。组件 watcher 不会立刻同步重渲染,而是进入 scheduler 队列,去重后在 nextTick 中批量 flush,避免一次事件里多次 set 导致多次渲染。

05

Vue2 的典型限制

defineProperty 只能劫持已有属性,所以新增属性和删除属性无法被自然追踪;数组通过索引赋值和直接改 length 也不会走变异方法。解决方式是 Vue.set、Vue.delete、splice,或者从一开始把响应式字段声明完整。

06

Vue3 Proxy 的改进

Proxy 代理对象本身,可以拦截属性读取、新增、删除、in、枚举等操作,集合类型也能做专门追踪。它让响应式覆盖面更完整,但解构 reactive 后丢失代理访问、ref 需要 .value、浅响应式和只读代理这些边界仍然需要说明。

易错点

  • 只说 defineProperty 能监听数据变化,却讲不出 getter 收集依赖、setter 派发更新这两个阶段。
  • 认为 Vue2 对新增属性、数组下标和 length 修改天然响应式,忽略 Vue.set、splice 等补救方式。
  • 把数据 set 和 DOM 立刻同步画等号,答不出 watcher 队列、去重和 nextTick 批量更新。
  • 把 Vue3 Proxy 说成没有任何边界,忽略 reactive 解构、ref .value、浅响应式和只读代理的使用差异。

面试官追问

为什么 Vue2 新增属性不是响应式?

因为 defineProperty 在初始化时只给已有 key 安装 getter/setter。后续直接 obj.newKey = value 只是普通赋值,没有 dep,也不会通知 watcher。

数组为什么要单独处理?

数组索引很多且 length 行为特殊,Vue2 没有逐个索引做 defineProperty,而是重写 push、pop、splice 等会改变数组的原型方法,在方法执行后通知更新。

computed 和 watch 在响应式里有什么区别?

computed 是惰性 watcher,依赖不变时读取缓存;watch 更偏副作用,当依赖变化后执行用户回调,适合异步请求、日志和桥接外部状态。

nextTick 解决的是什么问题?

它让同一轮同步代码里的多次数据修改先被收集和去重,再统一刷新 DOM。nextTick 回调执行时通常能拿到本轮更新后的 DOM 状态。