Skip to content

Vue3 响应式变化 ✨

要点速览

  • 拦截机制:Proxy 全对象拦截,集合类型可感知;ref 以 getter/setter 包裹原始值,reactive 返回代理。
  • 创建响应式:ref/reactive/shallowReactive/shallowRef/readonlytoRaw/markRaw 管理原始对象与非响应对象。
  • 依赖追踪:WeakMap → Map → Set 精细到属性级别;track/trigger 驱动副作用;effect 支持调度与停止。
  • 最佳实践:避免解构丢失响应性,使用 toRefs/toRef;派生数据用 computed,副作用用 watch/watchEffect

变化总览

Vue3 的响应式与 Vue2 相比主要变更集中在三处:

  1. 数据拦截:由 Object.defineProperty(属性级)升级为 Proxy(对象级),覆盖新增/删除属性、原型、集合类型等更多操作。
  2. 创建响应式:从 data() 的选项式初始化,转向 ref/reactive 家族的组合式声明;更清晰的原始值与对象响应模型。
  3. 依赖收集:从 Watcher + Dep(以组件/渲染为单位)到 WeakMap/Map/Set(属性级副作用函数),更新粒度更细、调度更灵活。

数据拦截

差异与能力边界

  • Vue2:Object.defineProperty 只能拦截已存在属性的读写,无法直接感知新增/删除属性、原型链变更、数组长度变化等;对 Map/Set 等集合类型支持不足。
  • Vue3:Proxy 拦截整个对象的操作(get/set/deleteProperty/has/ownKeys/getPrototypeOf/setPrototypeOf 等),天然覆盖属性新增/删除、原型访问、数组方法与集合类型操作。
  • 性能:在多数场景下 Proxy 更高效;同时减少了对深层递归定义拦截的需求。
js
// 基本代理处理思路(简化示意)
const reactiveHandlers = {
  get(target, key, receiver) {
    track(target, key);
    const res = Reflect.get(target, key, receiver);
    return isObject(res) ? reactive(res) : res;
  },
  set(target, key, value, receiver) {
    const old = target[key];
    const ok = Reflect.set(target, key, value, receiver);
    if (ok && old !== value) trigger(target, key);
    return ok;
  },
  deleteProperty(target, key) {
    const ok = Reflect.deleteProperty(target, key);
    if (ok) trigger(target, key);
    return ok;
  },
};

创建响应式

API 与适用场景

  • ref(value):封装原始值或对象的响应引用,通过 .value 读写;在模板中自动解包。
  • reactive(obj):返回对象的代理;应对嵌套结构更自然;不适用于原始值。
  • shallowRef/shallowReactive:仅浅层追踪,适合大型不可变结构或第三方实例。
  • readonly(obj):只读代理,约束外部不可修改;配合模块边界更安全。
  • toRaw/markRaw:获取原始对象或标记对象为非响应,避免不必要的追踪与开销。

部分源码

js
class RefImpl<T> {
  private _value: T
  private _rawValue: T

  public dep?: Dep = undefined
  public readonly __v_isRef = true

  constructor(
    value: T,
    public readonly __v_isShallow: boolean,
  ) {
    this._rawValue = __v_isShallow ? value : toRaw(value)
    // 有可能是原始值,有可能是 reactive 返回的 proxy
    this._value = __v_isShallow ? value : toReactive(value)
  }

  get value() {
    // 收集依赖 略
    return this._value
  }

  set value(newVal) {
    // 略
  }
}

// 判断是否是对象,是对象就用 reactive 来处理,否则返回原始值
export const toReactive = <T extends unknown>(value: T): T =>
  isObject(value) ? reactive(value) : value
js
function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>
) {
  // ...

  // 创建 Proxy 代理对象
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  );
  proxyMap.set(target, proxy);
  return proxy;
}

export function reactive(target: object) {
  // ...

  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers,
    reactiveMap
  );
}

依赖收集

  • Vue2:Watcher + Dep

    • 每个响应式属性都有一个 Dep 实例,用于做依赖收集,内部包含了一个 Set,存储依赖这个属性的所有 Watcher
    • 当属性值发生变化,Dep 就会通知所有的 Watcher 去做更新操作

示例(Vue2 的 Dep/Watcher 思路)

js
// Dep:依赖收集器,管理依赖当前属性的所有观察者(Watcher)
class Dep {
  constructor() {
    // 使用 Set 去重,避免同一观察者被重复收集
    this.subs = new Set();
  }
  // 在属性的 getter 中调用,完成依赖收集
  depend() {
    if (Dep.target) this.subs.add(Dep.target);
  }
  // 在属性的 setter 中调用,通知所有观察者更新
  notify() {
    this.subs.forEach((w) => w.update());
  }
}

// defineReactive:为指定对象属性定义响应式的 get/set 劫持
function defineReactive(obj, key, val) {
  const dep = new Dep();
  Object.defineProperty(obj, key, {
    // 读取属性时,收集当前处于激活状态的观察者(Dep.target)
    get() {
      dep.depend();
      return val;
    },
    // 写入属性时,如果值变更则通知依赖该属性的所有观察者重新计算
    set(newVal) {
      if (newVal !== val) {
        val = newVal;
        dep.notify();
      }
    },
  });
  return dep;
}

// Watcher:观察者,封装了一个 getter,用于读取依赖的响应属性
class Watcher {
  constructor(getter) {
    this.getter = getter;
    this.value = undefined;
    // 立即求值并完成依赖收集(触发被依赖属性的 getter)
    this.get();
  }
  // 激活当前观察者,执行 getter 以收集依赖,并保存当前值
  get() {
    Dep.target = this;
    this.value = this.getter();
    Dep.target = null;
    return this.value;
  }
  // 当依赖的属性发生变更时由 Dep 调用,重新求值
  update() {
    this.get();
  }
}

// 示例:为 state.count 定义响应式属性,并创建两个观察者
const state = {};
defineReactive(state, "count", 0);

// 观察者 w1:打印 count 的原值
const w1 = new Watcher(() => {
  console.log("w1", state.count);
  return state.count;
});

// 观察者 w2:打印 count 的两倍
const w2 = new Watcher(() => {
  console.log("w2", state.count * 2);
  return state.count * 2;
});

// 修改 count,会触发 dep.notify(),从而依次调用 w1.update()/w2.update()
state.count = 1;
state.count = 2;

核心机制概述:

  • 通过 Object.defineProperty 拦截属性的 get/set
  • 每个属性对应一个 Dep,内部用 Set 保存依赖当前属性的 Watcher
  • 读取属性(get)时,若存在激活的 Dep.target,则收集到 dep.subs
  • 修改属性(set)且新值不同,触发 dep.notify(),依次调用 Watcher.update() 重新运行其 getter。

运行流程:

  1. new Watcher(getter):设置自身为 Dep.target 并立即执行 getter 完成依赖收集。
  2. 之后对 state.count 的读取会被收集依赖;写入则通知相关 Watcher 更新并重新计算。
  • Vue3:WeakMap + Map + Set
    • Vue3 的依赖收集粒度更细
    • WeakMap 键对应的是响应式对象,值是一个 Map,这个 Map 的键是该对象的属性,值是一个 Set,Set 里面存储了所有依赖于这个属性的 effect 函数

示例(Vue3 的 WeakMap/Map/Set + effect 思路)

js
const targetMap = new WeakMap();
let activeEffect = null;

function effect(fn) {
  const eff = () => {
    activeEffect = eff;
    fn();
    activeEffect = null;
  };
  eff();
  return eff;
}

function track(target, key) {
  if (!activeEffect) return;
  let depsMap = targetMap.get(target);
  if (!depsMap) targetMap.set(target, (depsMap = new Map()));
  let dep = depsMap.get(key);
  if (!dep) depsMap.set(key, (dep = new Set()));
  dep.add(activeEffect);
}

function trigger(target, key) {
  const depsMap = targetMap.get(target);
  if (!depsMap) return;
  const dep = depsMap.get(key);
  dep && dep.forEach((eff) => eff());
}

function reactive(obj) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      const res = Reflect.get(target, key, receiver);
      track(target, key);
      return res;
    },
    set(target, key, value, receiver) {
      const old = target[key];
      const ok = Reflect.set(target, key, value, receiver);
      if (ok && old !== value) trigger(target, key);
      return ok;
    },
  });
}

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

const e1 = effect(() => {
  console.log("e1", state.count);
});

const e2 = effect(() => {
  console.log("e2", state.count * 2);
});

state.count = 1;
state.count = 2;

核心机制概述:

  • 使用 Proxy/Reflect 拦截对象属性的 get/set,实现依赖收集与触发更新。
  • 依赖存储结构:WeakMap(target) -> Map(key) -> Set(effects)
  • 全局变量 activeEffect 指向当前执行的副作用函数;在 effect(fn) 执行期间,读取被拦截属性会调用 track 收集依赖。
  • 属性写入成功且新旧值不同时调用 trigger,按 Set 中的副作用逐个重新执行,实现响应式更新。

运行流程:

  1. 调用 effect(fn):设置 activeEffect 并立即执行 fn 完成依赖收集(首轮运行)。
  2. 读取属性:触发 track,将 activeEffect 记录到对应的依赖集合。
  3. 写入属性:触发 trigger,取出依赖集合并逐个执行副作用函数,驱动视图或计算更新。

总结起来,Vue3 相比 Vue2 的依赖追踪粒度更细。Vue2 中 Dep 收集的是 Watcher 实例(如组件渲染 watcher、计算属性 watcher 等),而 Vue3 中收集的是属性级的副作用函数(effect),并以 WeakMap(target) → Map(key) → Set(effect) 的结构组织依赖。

面试题

说一说 Vue3 响应式相较于 Vue2 是否有改变?如果有,那么说一下具体有哪些改变?

参考答案:

相比较 Vue2,Vue3 在响应式的实现方面有这么一些方面的改变:

  1. 数据拦截从 Object.defineProperty 改为了 Proxy + Object.defineProperty 的拦截方式,其中
    • ref:使用 ObjectdefineProperty + Proxy 方式
    • reactive:使用 Proxy 方式
  2. 创建响应式数据在语法层面有了变化:
    • Vue2: 通过 data 来创建响应式数据
    • Vue3: 通过 ref、reactvie 等方法来创建响应式数据
  3. 依赖收集上面的变化
    • Vue2:Watcher + Dep
    • Vue3:WeakMap + Map + Set
    • 这种实现方式可以实现更细粒度的依赖追踪和更新控制