目录

一、先搞清楚两个概念:响应式 vs 双向绑定

二、Vue 2 响应式原理:Object.defineProperty

2.1 核心思路

2.2 源码实现:defineReactive

2.3 数据代理:把 data 挂到 this 上

2.4 Vue 2 的三大缺陷

三、Vue 3 响应式原理:Proxy

3.1 为什么用 Proxy?

3.2 核心数据结构:targetMap

3.3 reactive 源码实现

3.4 Handlers:get 拦截(依赖收集 track)

3.5 effect:副作用函数

3.6 ref 的实现

四、双向绑定:v-model 的本质

4.1 v-model 是语法糖

4.2 完整闭环

4.3 组件上的 v-model:Vue 3.4 的 defineModel

五、一张图总结

六、面试高频追问与回答策略

写在最后


读完这篇文章,你将彻底理解 Vue 的响应式系统是如何“感知”数据变化的,以及 v-model 这个“魔法”背后到底是什么。

很多前端开发者每天都在用 Vue,知道“数据变了视图会自动更新”,但一旦问到“怎么实现的”,就开始支支吾吾了。面试官想听的,不是背概念,而是你能从源码层面讲清楚:数据劫持、依赖收集、派发更新这三个环节是如何协同工作的。

今天这篇文章,带你从源码层面彻底搞懂 Vue 的响应式原理和双向绑定。

一、先搞清楚两个概念:响应式 vs 双向绑定

很多人把“响应式”和“双向绑定”混为一谈,其实它们是不同层面的东西

概念方向核心机制典型 API
响应式(Reactivity)数据 → 视图数据变化自动更新视图reactiverefcomputed
双向绑定(Two-way Binding)数据 ⇄ 视图数据变视图更新,视图变数据也更新v-model

响应式是“单向”的:数据变了,视图跟着变。双向绑定是“响应式 + 事件监听”:数据变视图更新,用户在视图上的操作(如输入)也能反过来修改数据。

双向绑定的本质是:响应式系统(数据→视图)+ DOM 事件监听(视图→数据) 。搞清楚这个关系,下面的源码分析你就能对号入座了。

二、Vue 2 响应式原理:Object.defineProperty

2.1 核心思路

Vue 2 的响应式系统核心是 Object.defineProperty —— 通过它给对象的每个属性定义 getter 和 setter,在属性被访问和修改时进行拦截。

Vue 2 的响应式源码主要分布在 core/observer 目录下,涉及三个核心角色:

角色职责
Observer递归遍历数据对象,用 defineReactive 把每个属性转为 getter/setter
Dep(Dependency)依赖收集器,每个响应式属性都有一个 Dep 实例,用来存放所有依赖它的 Watcher
Watcher观察者,负责执行更新操作(如渲染组件、执行 computed 回调)

2.2 源码实现:defineReactive

下面是 Vue 2 源码中 defineReactive 的核心逻辑(简化版):

function defineReactive(obj, key, val) {
  // 每个属性都有一个独立的 Dep 实例
  const dep = new Dep();
  
  // 递归处理嵌套对象
  const childOb = observe(val);
  
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      // 【依赖收集】当属性被读取时,将当前 Watcher 添加到 dep 中
      if (Dep.target) {
        dep.depend();
        if (childOb) {
          childOb.dep.depend();  // 数组/对象子属性也要收集
        }
      }
      return val;
    },
    set: function reactiveSetter(newVal) {
      if (newVal === val) return;
      val = newVal;
      // 【派发更新】当属性被修改时,通知所有依赖它的 Watcher
      dep.notify();
    }
  });
}

2.3 数据代理:把 data 挂到 this 上

我们在组件里写 this.name 就能访问到 data 中的 name,这也是通过 Object.defineProperty 做了一层代理

// Vue 源码中的 proxy 函数(简化版)
function proxy(target, sourceKey, key) {
  Object.defineProperty(target, key, {
    get: function() {
      return this[sourceKey][key];  // 实际返回 this._data.name
    },
    set: function(val) {
      this[sourceKey][key] = val;   // 实际设置 this._data.name = val
    }
  });
}

当我们写 this.name 时,实际上访问的是 this._data.name,而 _data 里的属性已经被 defineReactive 劫持过了。

2.4 Vue 2 的三大缺陷

这也是为什么 Vue 3 要重构响应式系统的根本原因:

  1. 无法监听对象属性的新增和删除Object.defineProperty 只能劫持已存在的属性,新增属性需要用 Vue.set

  2. 数组变更无法监听:通过下标修改数组(arr[0] = xxx)无法触发更新,所以要 hack 数组的 pushpop 等变异方法

  3. 深层监听性能问题:初始化时要递归遍历整个对象,嵌套越深性能越差

三、Vue 3 响应式原理:Proxy

3.1 为什么用 Proxy?

Vue 3 用 Proxy 全面重构了响应式系统,源码在 packages/reactivity 目录下。Proxy 直接代理整个对象,而不是对象的某个属性,所以:

  • ✅ 支持动态新增/删除属性

  • ✅ 支持数组下标修改

  • ✅ 惰性监听(只有访问到深层属性时才递归代理)

  • ✅ 原生支持 Map、Set 等集合类型

3.2 核心数据结构:targetMap

Vue 3 的依赖管理采用 WeakMap + Map + Set 三层结构:

// targetMap: 存储所有响应式对象的依赖关系
// WeakMap<target, Map<key, Set<effect>>>
const targetMap = new WeakMap();

// 结构示意:
// targetMap = {
//   userObj => {
//     'name' => [effect1, effect2],
//     'age'  => [effect1]
//   }
// }
  • WeakMap:键是响应式对象(弱引用,不影响垃圾回收)

  • Map:键是对象的属性名

  • Set:存储依赖这个属性的所有 effect(副作用函数)

3.3 reactive 源码实现

reactive 的入口在 packages/reactivity/src/reactive.ts

// reactive 入口
export function reactive(target: object) {
  if (isReadonly(target)) {
    return target;
  }
  return createReactiveObject(
    target,
    false,
    mutableHandlers,        // 普通对象的 handlers
    mutableCollectionHandlers, // 集合类型的 handlers
    reactiveMap
  );
}

// 创建响应式代理对象
function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers, proxyMap) {
  // 1. 如果不是对象,直接返回
  if (!isObject(target)) return target;
  
  // 2. 如果已经是代理对象,直接返回
  if (target[ReactiveFlags.RAW]) return target;
  
  // 3. 如果已经代理过了,从缓存中取
  const existingProxy = proxyMap.get(target);
  if (existingProxy) return existingProxy;
  
  // 4. 创建 Proxy 代理
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  );
  
  // 5. 缓存代理结果
  proxyMap.set(target, proxy);
  return proxy;
}

3.4 Handlers:get 拦截(依赖收集 track)

当访问响应式对象的属性时,会触发 get 拦截器,执行 依赖收集(track) :

const mutableHandlers = {
  get(target, key, receiver) {
    // 如果是特殊标记,直接返回
    if (key === ReactiveFlags.IS_REACTIVE) return true;
    
    // 【依赖收集】将当前正在执行的 effect 与这个属性关联起来
    track(target, key);
    
    // 获取值
    const res = Reflect.get(target, key, receiver);
    
    // 【惰性代理】如果取到的值还是对象,递归代理(只有在访问时才代理,不是初始化时)
    if (isObject(res)) {
      return reactive(res);
    }
    return res;
  },
  
  set(target, key, value, receiver) {
    const oldValue = target[key];
    const result = Reflect.set(target, key, value, receiver);
    
    // 【派发更新】只有值真正变化时才触发更新
    if (oldValue !== value) {
      trigger(target, key, value, oldValue);
    }
    return result;
  }
};

3.5 effect:副作用函数

effect 是 Vue 3 响应式系统的“发动机”:

// effect 函数:创建一个响应式副作用
function effect(fn) {
  const _effect = new ReactiveEffect(fn);
  _effect.run();  // 立即执行,触发依赖收集
  return _effect;
}

class ReactiveEffect {
  active = true;
  deps = [];  // 记录这个 effect 依赖了哪些属性
  
  constructor(fn) {
    this.fn = fn;
  }
  
  run() {
    if (!this.active) return this.fn();
    
    // 将当前 effect 设置为全局激活状态
    const lastEffect = activeEffect;
    activeEffect = this;
    
    // 执行 fn → 内部访问响应式数据 → 触发 get → 触发 track 收集依赖
    const result = this.fn();
    
    activeEffect = lastEffect;  // 恢复
    return result;
  }
}

整个流程effect(fn) 执行 → fn 内部读取响应式数据 → 触发 get 拦截 → track 把当前 effect 存进 targetMap → 数据修改时触发 set → trigger 从 targetMap 取出所有 effect 重新执行。

3.6 ref 的实现

ref 是用来包装基本类型的,它的核心是创建一个带有 .value 属性的对象,同样通过 getter/setter 实现依赖收集和派发更新:

function ref(value) {
  return createRef(value, false);
}

function createRef(rawValue, shallow) {
  return new RefImpl(rawValue, shallow);
}

class RefImpl {
  _value;
  _rawValue;
  dep;  // 每个 ref 有自己的 Dep
  
  constructor(value) {
    this._rawValue = value;
    this._value = convert(value);  // 如果是对象,用 reactive 包装
    this.dep = new Dep();
  }
  
  get value() {
    // 依赖收集
    track(this, 'value');
    return this._value;
  }
  
  set value(newVal) {
    if (newVal !== this._rawValue) {
      this._rawValue = newVal;
      this._value = convert(newVal);
      // 派发更新
      trigger(this, 'value');
    }
  }
}

四、双向绑定:v-model 的本质

搞懂了响应式系统(数据 → 视图),我们来看双向绑定的另一半(视图 → 数据)。

4.1 v-model 是语法糖

v-model 本质上就是 :value + @input 的语法糖

<!-- 你写的 -->
<input v-model="user.name" />

<!-- 编译后实际上变成 -->
<input :value="user.name" @input="user.name = $event.target.value" />

$event.target.value 是原生 DOM 事件对象中的输入值。

4.2 完整闭环

双向绑定的完整流程是:

  1. 数据 → 视图:响应式系统的 setter 触发 → dep.notify() → Watcher 更新 → 视图重新渲染,input 的 value 被更新

  2. 视图 → 数据:用户在输入框中打字 → 触发 input 事件 → 执行 user.name = $event.target.value → 触发响应式数据的 setter → 回到步骤 1

这就形成了一个闭环:用户输入改变数据,数据改变又驱动视图更新。

4.3 组件上的 v-model:Vue 3.4 的 defineModel

在自定义组件上使用 v-model 时,Vue 3.4 引入了 defineModel 宏,让双向绑定更简洁:

<!-- 父组件 -->
<Child v-model="count" />

<!-- 子组件(Vue 3.4+) -->
<script setup>
// defineModel 返回一个 ref,与父组件的 v-model 双向同步
const model = defineModel();
// 修改 model.value 会自动同步到父组件
model.value++;
</script>

defineModel 底层依然是 props + emits 的模式,只是把模板代码封装成了宏。

五、一张图总结

六、面试高频追问与回答策略

Q1:Vue 2 和 Vue 3 的响应式有什么区别?

Vue 2 用 Object.defineProperty 劫持对象属性,缺点是无法监听新增/删除属性和数组下标修改,且初始化时要递归遍历整个对象。Vue 3 用 Proxy 代理整个对象,支持动态属性、数组变更、集合类型,且采用惰性代理——只有访问到深层属性时才递归,性能更好。

Q2:v-model 和 v-bind 有什么区别?

v-bind 是单向绑定(数据→视图),v-model 是双向绑定(数据⇄视图)。v-model 本质上是 v-bind:value + @input 事件监听的语法糖。

Q3:reactive 和 ref 有什么区别?什么时候用哪个?

reactive 只能代理对象,返回的是 Proxy 代理对象;ref 可以包装任意类型(包括基本类型),返回的是带有 .value 属性的 RefImpl 实例。对象用 reactive,基本类型用 ref;如果解构 reactive 对象会丢失响应性,需要用 toRefs 转换。

写在最后

Vue 的响应式系统,本质上就是 “数据劫持 + 发布-订阅模式”

  • 数据劫持:Vue 2 用 Object.defineProperty,Vue 3 用 Proxy,拦截数据的读和写

  • 依赖收集:在数据被读取时(getter),把当前正在执行的 Watcher/Effect 记录下来

  • 派发更新:在数据被修改时(setter),通知所有依赖它的 Watcher/Effect 重新执行

双向绑定则是在这个基础上,加上了 DOM 事件监听(@input),让视图的变化能反向修改数据。

理解了这套机制,你就能明白为什么 Vue.set 存在、为什么数组有些操作不触发更新、为什么 ref 要用 .value 访问——所有 API 设计背后,都有源码层面的必然逻辑

(PS:本文由deepseek整理生成)

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐