WSの小屋

前言

Vue 最核心的特性之一就是响应式系统。当数据发生变化时,视图会自动更新,这看似魔法般的体验背后,是精心设计的依赖追踪与派发更新机制。本文将从 Vue 2 的 Object.defineProperty 到 Vue 3 的 Proxy,深入剖析响应式系统的演化与实现原理。


一、什么是响应式?

响应式的本质是:当数据变化时,自动执行副作用

let A = 1;
let B = 2;
let C = A + B;

// 当 A 或 B 变化时,C 应该自动更新
A = 10;
// C 应该变成 12

在命令式编程中,你需要手动重新计算 C。而在响应式系统中,这一切是自动的。

常见的响应式范式

  1. 观察者模式:数据持有订阅者列表,变化时通知所有订阅者(Vue 2.x)
  2. 信号/可观察对象:数据封装为可观察单元,如 SolidJS 的 Signals、RxJS 的 Observable
  3. 代理拦截:通过 Proxy 拦截数据操作,自动追踪依赖(Vue 3.x)

二、Vue 2 的响应式:Object.defineProperty

2.1 基本原理

Vue 2 使用 Object.defineProperty 将对象属性转换为 getter/setter:

function defineReactive(obj, key, val) {
  let dep = new Dep(); // 依赖收集器

  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get() {
      // 收集依赖:当前正在执行的 Watcher 会被添加到 dep 中
      if (Dep.target) {
      dep.depend();
      }
      return val;
    },
    set(newVal) {
      if (newVal === val) return;
      val = newVal;
      // 通知所有依赖更新
      dep.notify();
    }
  });
}

2.2 Dep 与 Watcher

  • Dep(依赖):每个响应式属性都有一个 Dep 实例,维护一个 Watcher 订阅者列表。
  • Watcher(观察者):当组件渲染或 computed 求值时,会创建 Watcher,在执行过程中访问响应式属性,触发 getter,从而被收集到对应 Dep 中。
class Dep {
  constructor() {
    this.subs = [];
  }

  depend() {
    if (Dep.target) {
      Dep.target.addDep(this);
    }
  }

  addSub(sub) {
    this.subs.push(sub);
  }

  notify() {
    this.subs.forEach(sub => sub.update());
  }
}

Dep.target = null;

2.3 对象响应化

function observe(obj) {
  if (typeof obj !== 'object' || obj === null) return;

  Object.keys(obj).forEach(key => {
    let internalValue = obj[key];
    // 递归处理嵌套对象
    observe(internalValue);
    defineReactive(obj, key, internalValue);
  });
}

2.4 数组处理

Object.defineProperty 无法拦截数组索引修改和 length 变化,Vue 2 通过重写数组原型方法来解决:

const arrayProto = Array.prototype;
const arrayMethods = Object.create(arrayProto);

['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(method => {
  const original = arrayProto[method];
  arrayMethods[method] = function (...args) {
    const result = original.apply(this, args);
    const dep = this.__dep__;

    // 对新增元素进行响应化
    let inserted;
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args;
        break;
      case 'splice':
        inserted = args.slice(2);
        break;
    }
    if (inserted) observeArray(inserted);

    dep.notify();
    return result;
  };
});

2.5 Vue 2 响应式的局限性

  1. 无法检测属性添加/删除obj.newKey = value 不会触发响应。需要 Vue.set(obj, 'newKey', value)
  2. 无法检测数组索引赋值arr[0] = newValue 不触发响应。需要 Vue.set(arr, 0, newValue)arr.splice(0, 1, newValue)
  3. 初始化递归深拷贝:性能开销大,尤其是大型对象。
  4. 属性级别的拦截:每个属性都需要单独定义 getter/setter,对大量属性的对象初始化较慢。

三、Vue 3 的响应式:Proxy

3.1 Proxy 简介

ES6 的 Proxy 可以代理整个对象,拦截包括属性读取、设置、删除、遍历等 13 种操作:

const proxy = new Proxy(target, handler);

3.2 reactive 实现

const reactiveMap = new WeakMap();

function reactive(target) {
  // 非对象直接返回
  if (typeof target !== 'object' || target === null) return target;

  // 如果已经是代理,直接返回
  if (target.__v_isReactive) return target;

  // 缓存检查
  if (reactiveMap.has(target)) {
    return reactiveMap.get(target);
  }

  const proxy = new Proxy(target, {
    get(target, key, receiver) {
      if (key === '__v_isReactive') return true;

      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 hadKey = Array.isArray(target)
        ? Number(key) < target.length
        : Object.prototype.hasOwnProperty.call(target, key);

      const result = Reflect.set(target, key, value, receiver);

      // 只有值真正变化时才触发更新
      if (!hadKey) {
        // 新增属性
        trigger(target, 'add', key);
      } else if (value !== oldValue && (oldValue === oldValue || value === value)) {
        // 修改属性
        trigger(target, 'set', key);
      }

      return result;
    },
    deleteProperty(target, key) {
      const hadKey = Object.prototype.hasOwnProperty.call(target, key);
      const result = Reflect.deleteProperty(target, key);
      if (hadKey && result) {
        trigger(target, 'delete', key);
      }
      return result;
    }
  });

  reactiveMap.set(target, proxy);
  return proxy;
}

3.3 track 与 trigger

// targetMap: target -> key -> dep
const targetMap = new WeakMap();

// 当前活跃的 effect
let activeEffect = null;

function track(target, key) {
  if (!activeEffect) return;

  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);
  // 反向记录,方便清理
  activeEffect.deps.push(dep);
}

function trigger(target, type, key) {
  const depsMap = targetMap.get(target);
  if (!depsMap) return;

  const dep = depsMap.get(key);
  if (dep) {
    // 创建副本执行,避免无限循环
    [...dep].forEach(effect => effect.run());
  }
}

3.4 effect 函数

function effect(fn, options = {}) {
  const _effect = {
    fn,
    scheduler: options.scheduler,
    deps: [],
    run() {
      activeEffect = this;
      cleanup(this); // 清除旧依赖
      try {
        return this.fn();
      } finally {
        activeEffect = null;
      }
    }
  };

  _effect.run();

  const runner = _effect.run.bind(_effect);
  runner.effect = _effect;
  return runner;
}

function cleanup(effect) {
  effect.deps.forEach(dep => dep.delete(effect));
  effect.deps.length = 0;
}

3.5 ref 实现

ref 用于包装基本类型:

function ref(value) {
  return {
    get value() {
      track(this, 'value');
      return value;
    },
    set value(newVal) {
      if (newVal !== value) {
        value = newVal;
        trigger(this, 'set', 'value');
      }
    }
  };
}
``n
### 3.6 computed 实现

```js
function computed(getter) {
  let value;
  let dirty = true;

  const _effect = effect(getter, {
    scheduler() {
      // 依赖变化时标记为脏值,不立即计算
      dirty = true;
      // 手动触发依赖 computed 的 effect
      trigger(computedObj, 'set', 'value');
    }
  });

  const computedObj = {
    get value() {
      if (dirty) {
        value = _effect.run();
        dirty = false;
      }
      track(computedObj, 'value');
      return value;
    }
  };

  return computedObj;
}

四、Proxy 相比 defineProperty 的优势

特性 Object.defineProperty (Vue 2) Proxy (Vue 3)
拦截范围 属性级别,仅 get/set 对象级别,13 种操作
新增属性 ❌ 无法检测 ✅ 自动检测
删除属性 ❌ 无法检测 ✅ 自动检测
数组索引修改 ❌ 无法检测 ✅ 自动检测
数组 length 变化 ❌ 无法检测 ✅ 自动检测
Map/Set 支持 ❌ 不支持 ✅ 支持
初始化方式 递归遍历所有属性 惰性代理,访问时响应化
性能(大量属性) 初始化慢 初始化快,按需代理
浏览器支持 IE9+ ES6+(不支持 IE)

五、Vue 3 响应式的高级特性

5.1 嵌套响应式:惰性递归

Vue 2 在初始化时递归遍历整个对象。Vue 3 采用惰性策略:只有在访问到嵌套属性时才将其响应化。

// Vue 2:初始化时全部递归
const state = Vue.observable({
  user: { name: 'Vue', address: { city: 'Hangzhou' } }
});
// address 已经被递归响应化,即使你不需要

// Vue 3:访问时才响应化
const state = reactive({
  user: { name: 'Vue', address: { city: 'Hangzhou' } }
});
// 此时 address 还未被响应化
console.log(state.user.address.city);
// 只有访问 state.user.address 时才会代理 address

5.2 集合类型支持

Vue 3 可以代理 Map、Set、WeakMap、WeakSet,通过自定义的集合处理器:

const collectionHandlers = {
  get(target, key, receiver) {
    // 对集合方法进行特殊处理
    if (key === 'size') return Reflect.get(target, key, target);
    if (key === 'has') return target.has.bind(target);
    if (key === 'add') {
      return function(value) {
        const hadKey = target.has(value);
        const result = target.add(value);
        if (!hadKey) trigger(target, 'add', value);
        return result;
      };
    }
    // ... 其他方法
    return Reflect.get(target, key, receiver);
  }
};

5.3 ref vs reactive 使用指南

// 基本类型用 ref
const count = ref(0);
const name = ref('Vue');

// 对象用 reactive
const state = reactive({
  user: { name: 'Vue' },
  list: [1, 2, 3]
});

// 组合使用
const searchState = reactive({
  keyword: ref(''),
  page: ref(1)
});
// ref 在 reactive 中会自动解包
console.log(searchState.keyword); // '',不需要 .value

5.4 shallowReactive 与 shallowRef

当你只需要浅层响应式时:

import { shallowReactive, shallowRef } from 'vue';

// 浅层响应式:只有第一层属性是响应式的
const state = shallowReactive({
  user: { name: 'Vue' }
});

state.user = { name: 'React' }; // ✅ 触发更新
state.user.name = 'React';      // ❌ 不触发更新

// shallowRef:只有 .value 赋值才触发
const data = shallowRef({ count: 0 });
data.value = { count: 1 };         // ✅ 触发更新
data.value.count = 1;              // ❌ 不触发更新
data.value = { count: 1 };          // ✅ 整体替换才触发

5.5 readonly 与 shallowReadonly

import { readonly } from 'vue';

const original = reactive({ count: 0 });
const copy = readonly(original);

console.log(copy.count); // 0
copy.count = 1; // ⚠️ warning: Set operation on key "count" failed: target is readonly

5.6 toRef 与 toRefs

import { reactive, toRef, toRefs } from 'vue';

const state = reactive({
  name: 'Vue',
  age: 3
});

// toRef:保持对源对象属性的响应式引用
const nameRef = toRef(state, 'name');

// toRefs:解构后保持响应式
const { name, age } = toRefs(state);
// 现在 name 和 age 是 ref,修改它们会同步到 state

5.7 markRaw:永久标记为非响应式

import { markRaw, reactive } from 'vue';

const heavyObject = markRaw({
  // 大量数据,不希望被代理
  data: new Array(100000).fill(0)
});

const state = reactive({
  items: heavyObject // 不会被响应化
});

六、effect 的嵌套与调度

6.1 嵌套 effect

const state = reactive({
  outer: 1, inner: 2
});

effect(() => {
  console.log('outer', state.outer);
  effect(() => {
    console.log('inner', state.inner);
  });
});

state.outer = 10;
// outer 1
// inner 2
// outer 10  <- 外层 effect 重新执行
// inner 2   <- 内层 effect 也会重新执行(因为是嵌套定义)

state.inner = 20;
// inner 20  <- 只有内层 effect 执行

Vue 3 使用栈结构管理 activeEffect,嵌套时父 effect 会被压栈,子 effect 完成后恢复。

6.2 调度执行:scheduler

const state = reactive({ count: 0 });

const runner = effect(() => {
  console.log('count:', state.count);
}, {
  scheduler() {
    // 不立即执行,而是放入微任务队列
    Promise.resolve().then(runner);
  }
});

state.count = 1;
state.count = 2;
state.count = 3;
// 只会输出:count: 0 
// 然后输出:count: 3(合并了多次更新)

这正是 Vue 组件更新的核心机制——通过 queueJob 将多次状态变更合并为一次 DOM 更新。


七、依赖清理与分支切换

7.1 为什么要清理?

const state = reactive({ flag: true, a: 1, b: 2 });

effect(() => {
  // 条件分支:flag 决定依赖 a 还是 b
  console.log(state.flag ? state.a : state.b);
});

// 初始执行:依赖 flag, a
state.b = 20;  // 不应该触发 effect

// flag 切换后:依赖 flag, b
state.flag = false;
state.a = 10;  // 不应该触发 effect(因为现在不依赖 a 了)

7.2 清理机制

每次 effect 重 defore 重新执行前,会清除所有旧依赖,然后在执行过程中重新收集。这样就能正确处理分支切换:

function effect(fn) {
  const _effect = {
    fn,
    deps: [],
    run() {
      activeEffect = this;
      cleanup(this); // 清除旧依赖
      try {
        return this.fn();
      } finally {
        activeEffect = null;
      }
    }
  };
  _effect.run();
  return _effect;
}

但这会带来一个经典问题:无限循环。如果在遍历 dep 的过程中删除自身,会导致 Set 遍历异常。解决方式是在 trigger 时创建副本:

function trigger(target, type, key) {
  const dep = getDep(target, key);
  if (!dep) return;
  // 创建副本执行,避免 cleanup + add 导致死循环
  [...dep].forEach(effect => effect.run());
}

八、总结

Vue 响应式系统从 Vue 2 的 Object.defineProperty 演化到 Vue 3 的 Proxy,经历了从「属性级拦截」到「对象级代理」的跨越:

  1. Vue 2:基于 getter/setter 的属性拦截,实现成熟但局限性明显(无法检测新增/删除属性、数组索引修改等),需要 Vue.set / Vue.delete 等辅助 API。

  2. Vue 3:基于 Proxy 的全对象代理,解决了 Vue 2 的所有响应式缺陷。引入了 track/trigger 的依赖收集/触发机制、effect 栈式管理、惰性响应化等现代设计,性能更优、能力更强。

理解响应式原理不仅能帮助你写出更高效的 Vue 代码,更是深入理解现代前端框架设计思路的基础。在下一篇文章中,我们将动手实现一个完整的 mini 响应式系统,用约 100 行代码还原 Vue 3 响应式的核心能力。


延伸阅读

Comments | 0条评论