Vue的响应式原理

摘要 :Vue 3 的响应式系统用 ES6 Proxy + Reflect 替代了 Vue 2 的 Object.defineProperty,解决了无法检测属性增删、数组索引修改等痛点。核心由四个函数协作完成------reactive 创建响应式代理、effect 注册副作用函数、track 在读取时收集依赖、trigger 在修改时触发更新。这四者构成了一个"数据变化 → 自动更新视图"的完整闭环。

一、Vue 2 vs Vue 3:为什么换掉 Object.defineProperty?

Object.defineProperty 的局限(Vue 2)

js 复制代码
// Vue 2 只能拦截「已有属性」的读写
Object.defineProperty(obj, 'a', {
  get() { /* ... */ },
  set(val) { /* ... */ }
});

obj.a = 1;        // ✅ 能检测到
obj.b = 2;        // ❌ 新增属性,检测不到!
delete obj.a;     // ❌ 删除属性,检测不到!
arr[0] = 'x';     // ❌ 数组索引修改,检测不到!
arr.push(1);      // ❌ 数组方法变异,需要 hack 重写

Proxy 的优势(Vue 3)

js 复制代码
// Proxy 拦截的是整个对象,不关心具体属性
const proxy = new Proxy(target, {
  get(target, key) { /* 拦截所有读取 */ },
  set(target, key, val) { /* 拦截所有写入 */ },
  deleteProperty(target, key) { /* 拦截删除 */ },
  has(target, key) { /* 拦截 in 操作 */ }
});

proxy.newProp = 1;   // ✅ 新增属性能检测到
delete proxy.a;      // ✅ 删除属性能检测到
proxy.arr[0] = 'x';  // ✅ 数组索引修改能检测到

对比总结

维度 Vue 2: Object.defineProperty Vue 3: Proxy
拦截粒度 逐个属性定义 getter/setter 整个对象统一代理
属性新增/删除 需要手动 $set / $delete 自动响应
数组 需要重写 7 个变异方法 原生支持
Map / Set / WeakMap 不支持 支持
性能 初始化时递归遍历所有属性 懒代理,访问时才处理

二、核心架构

javascript 复制代码
┌──────────┐     ┌─────────────┐     ┌─────────┐
│ compiler │     │ reactivity  │     │ runtime │
│  编译器   │     │  响应式核心  │     │ 运行时  │
└──────────┘     └──────┬──────┘     └─────────┘
                        │
              ┌─────────┴─────────┐
              │                   │
    Object.defineProperty      Proxy + Reflect
       (Vue 2)                (Vue 3)

reactivity 模块是响应式的核心,它不依赖 compiler 和 runtime,可以独立使用。


三、四大核心函数

1. reactive --- 创建响应式对象

Proxy 包装原始对象,返回一个代理。对代理的任何操作都会被拦截。

js 复制代码
function reactive(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      const result = Reflect.get(target, key, receiver);
      // 读取时:收集依赖 ------ "谁在读这个属性?"
      track(target, key);

      // 如果值是对象,递归包装(懒代理)
      if (typeof result === 'object' && result !== null) {
        return reactive(result);
      }

      return result;
    },

    set(target, key, value, receiver) {
      const oldValue = target[key];
      const result = Reflect.set(target, key, value, receiver);

      // 写入时:值真的变了才触发更新
      if (oldValue !== value) {
        trigger(target, key);
      }

      return result;
    },

    deleteProperty(target, key) {
      const result = Reflect.deleteProperty(target, key);
      if (result) {
        trigger(target, key);
      }
      return result;
    }
  });
}

为什么用 Reflect

  • Reflect.get/setProxy 的 handler 参数一一对应,保证 this 指向正确
  • 直接操作原对象会绕过 Proxy,而 Reflect 会走代理链
  • 返回布尔值,方便判断操作是否成功

2. effect --- 注册副作用函数

副作用函数就是"当依赖的数据变化时,需要重新执行的函数"。它是 computed 和 watch 的基础。

js 复制代码
// 全局变量:当前正在执行的 effect
let activeEffect = null;

function effect(fn) {
  const effectFn = () => {
    activeEffect = effectFn;  // 标记当前 effect
    fn();                     // 执行函数 → 触发 getter → 收集依赖
    activeEffect = null;      // 执行完清除标记
  };

  effectFn();  // 立即执行一次
  return effectFn;
}

使用示例:

js 复制代码
const state = reactive({ count: 0 });

effect(() => {
  console.log('count 变了:', state.count);
  // 这里读取了 state.count → 触发 getter → track 收集依赖
  // 此时 activeEffect 就是当前的 effect 函数
});

state.count = 1;  // 触发 setter → trigger → 重新执行 effect → 输出 "count 变了: 1"

3. track --- 依赖收集(在 getter 中调用)

记录"哪个 effect 依赖了哪个对象的哪个属性"。

js 复制代码
// 数据结构:target → key → Set<effect>
// 即:某个对象的某个属性 → 被哪些 effect 使用
const targetMap = new WeakMap();

function track(target, key) {
  if (!activeEffect) return;  // 没有 effect 在执行,不用收集

  let depsMap = targetMap.get(target);
  if (!depsMap) {
    depsMap = new Map();
    targetMap.set(target, depsMap);
  }

  let dep = depsMap.get(key);
  if (!dep) {
    dep = new Set();
    depsMap.set(key, dep);
  }

  dep.add(activeEffect);  // 把当前 effect 加入依赖集合
}

数据结构示意:

javascript 复制代码
targetMap (WeakMap)
  └── target (原始对象)
       └── depsMap (Map)
            ├── 'count' → Set [effectA, effectB]
            └── 'name'  → Set [effectA]

4. trigger --- 触发更新(在 setter 中调用)

找到依赖了这个属性的所有 effect,重新执行它们。

js 复制代码
function trigger(target, key) {
  const depsMap = targetMap.get(target);
  if (!depsMap) return;

  const dep = depsMap.get(key);
  if (!dep) return;

  // 复制一份再遍历,避免在执行过程中修改 Set 导致问题
  [...dep].forEach(effect => {
    effect();
  });
}

四、完整流程串联

用一个具体例子走一遍完整流程:

js 复制代码
// 第一步:创建响应式数据
const state = reactive({ count: 0, name: 'vue' });

// 第二步:注册副作用(自动收集依赖)
effect(() => {
  console.log(`count=${state.count}, name=${state.name}`);
});
/* 执行过程:
   1. activeEffect = 当前 effectFn
   2. 执行 fn()
   3. 读 state.count → get 拦截 → track(state, 'count') → 记录依赖
   4. 读 state.name  → get 拦截 → track(state, 'name')  → 记录依赖
   5. activeEffect = null
   输出: count=0, name=vue
*/

// 第三步:修改数据(触发更新)
state.count = 1;
/* 执行过程:
   1. set 拦截 → Reflect.set → 值变了
   2. trigger(state, 'count')
   3. 找到依赖 count 的所有 effect → 重新执行
   输出: count=1, name=vue
*/

流程图

scss 复制代码
  reactive(data)          effect(fn)
       │                      │
       ▼                      ▼
  ┌─────────┐           ┌──────────┐
  │ Proxy   │           │ 标记为    │
  │ 代理对象 │           │ active   │
  └────┬────┘           └─────┬────┘
       │                      │
       │    读取属性 (get)     │
       ◄──────────────────────┘
       │
       ▼
   track(target, key)
       │
       ▼
  targetMap 存入依赖关系
  { target: { key: [effect] } }

       │    修改属性 (set)
       ◄──────────────────────
       │
       ▼
  trigger(target, key)
       │
       ▼
  找到对应 effect → 重新执行 fn
  (回到 effect 开始新一轮循环)

五、从源码到实际 API

上面的是简化版原理,Vue 3 实际暴露的 API:

原理函数 对应的 Vue API 用途
reactive() reactive() 创建深层响应式对象
reactive() 浅层版 shallowReactive() 只代理第一层
reactive() + 只读 readonly() 响应式但不可修改
effect() watchEffect() 自动追踪依赖并执行回调
effect() + 显式指定 watch(() => src.value, cb) 监听特定来源
effect() + 缓存 computed(() => expr) 计算属性(带缓存)

computed 的本质

computed 就是一个带缓存的 effect:

js 复制代码
function computed(getter) {
  let value;
  let dirty = true;  // 脏标记:是否需要重新计算

  const effectFn = effect(() => {
    value = getter();
    dirty = false;
  });

  return {
    get value() {
      if (dirty) {
        effectFn();  // 重新计算
      }
      return value;  // 否则直接返回缓存
    }
  };
}

六、快速记忆口诀

  • reactive:Proxy 包一层,读写全拦截
  • effect:包住你的函数,执行时自动"报名"
  • track:getter 里喊一声"谁在看我",记下来
  • trigger:setter 里喊一声"我变了",通知所有人重来
  • 一句话概括:读的时候收债(collect),写的时候还钱(notify)
相关推荐
做前端的娜娜子1 天前
Web缓存策略有哪些?
掘金·金石计划
光影少年9 天前
RN 原生动画 Animated 用法、动画性能优化
react native·react.js·掘金·金石计划
光影少年12 天前
RN适配方案:屏幕适配、分辨率适配、刘海屏/全面屏适配
react native·react.js·掘金·金石计划
光影少年12 天前
RN中的StyleSheet.create 好处、内联样式弊端
react native·react.js·掘金·金石计划
光影少年14 天前
RN 样式特点:Flex 布局默认、没有像素单位、样式不能级联
react native·react.js·掘金·金石计划
光影少年21 天前
react navite JSBridge 通信机制、异步通信特点
react native·react.js·掘金·金石计划