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 天前
文本切片与召回(Chunk、Overlap 到混合检索)学习笔记
langchain·openai·掘金·金石计划
做前端的娜娜子1 天前
#浏览器存储方案:localStorage、sessionStorage 与 Cookie
前端·面试·掘金·金石计划
光影少年2 天前
react navite性能优化 & 常见坑
前端·react native·掘金·金石计划
做前端的娜娜子5 天前
文档加载工程:多格式数据接入与 Document 标准化
langchain·ai编程·掘金·金石计划
做前端的娜娜子6 天前
Embedding 向量模型:从语义表示到相似度计算
langchain·openai·掘金·金石计划
做前端的娜娜子6 天前
第二个 AI 调用——invoke 阻塞式与 stream 流式生成
人工智能·llm·掘金·金石计划
做前端的娜娜子7 天前
第一个AI调用——TypeScript 工程初始化与配置管理(实战跟练)
langchain·openai·掘金·金石计划
做前端的娜娜子9 天前
什么是浅拷贝(Shallow Copy)和深拷贝(Deep Copy)?如何手动实现一个深拷贝函数
javascript·面试·掘金·金石计划
做前端的娜娜子10 天前
JavaScript 闭包
前端·javascript·掘金·金石计划
做前端的娜娜子11 天前
移动端上拉加载与下拉刷新实现方案
前端·面试·掘金·金石计划