vue3 reactive解析

23 阅读5分钟

Vue3 的 reactive 是实现响应式数据的核心 API 之一,它基于 Proxy 代理 实现,相比 Vue2 的 Object.defineProperty 具有更强的能力(支持数组、新增属性、Map/Set 等)。下面我会从核心原理、源码拆解、关键逻辑三个维度,由浅入深解析 reactive 的实现。

一、核心原理概述

reactive 的本质是:通过 Proxy 拦截目标对象的读取、修改、删除等操作,在读取时收集依赖(track),在修改时触发依赖更新(trigger) ,从而实现数据变化驱动视图更新。

二、源码核心拆解(简化版)

Vue3 源码中 reactive 相关逻辑主要在 packages/reactivity/src/reactive.ts 文件中,下面是关键代码的简化版(保留核心逻辑,剔除边界处理),方便你理解:

1. 核心常量与工具函数

// 1. 存储响应式对象的缓存(避免重复代理)
const reactiveMap = new WeakMap();

// 2. 标记响应式对象的唯一标识
export const ReactiveFlags = {
  IS_REACTIVE: '__v_isReactive', // 标记是否为reactive对象
  RAW: '__v_raw' // 指向原始对象
};

// 3. 判断是否为可代理的对象(排除基本类型、null等)
function isObject(value) {
  return typeof value === 'object' && value !== null;
}

// 4. 判断是否为只读/非响应式对象(边界处理)
function canObserve(value) {
  return !value[ReactiveFlags.IS_READONLY] && isObject(value);
}

2. reactive 主函数

export function reactive(target) {
  // 边界1:如果目标是只读对象,直接返回
  if (target && target[ReactiveFlags.IS_READONLY]) {
    return target;
  }
  // 边界2:如果目标已经是响应式对象,直接返回代理对象
  const existingProxy = reactiveMap.get(target);
  if (existingProxy) {
    return existingProxy;
  }
  // 边界3:非对象类型(如字符串/数字),无法代理,直接返回
  if (!isObject(target)) {
    return target;
  }

  // 核心:创建Proxy代理
  const proxy = new Proxy(target, {
    // 拦截读取操作(如 obj.xxx、obj['xxx']、in 操作、for...in 等)
    get(target, key, receiver) {
      // 特殊key:判断是否为reactive对象
      if (key === ReactiveFlags.IS_REACTIVE) {
        return true;
      }
      // 特殊key:获取原始对象
      if (key === ReactiveFlags.RAW) {
        return target;
      }

      // 读取原始值(处理继承/原型链)
      const res = Reflect.get(target, key, receiver);

      // 收集依赖:只有非特殊key才收集
      if (!isSymbol(key) && !isNonTrackableKeys(key)) {
        track(target, 'get', key); // 核心:收集依赖
      }

      // 深度代理:如果返回值是对象,递归转为reactive
      if (isObject(res)) {
        return reactive(res);
      }

      return res;
    },

    // 拦截修改操作(如 obj.xxx = 123)
    set(target, key, value, receiver) {
      // 获取旧值,用于对比
      const oldValue = Reflect.get(target, key, receiver);
      // 如果是新增属性(旧值为undefined且key不在对象中)
      const hadKey = hasOwn(target, key);

      // 执行赋值操作
      const result = Reflect.set(target, key, value, receiver);

      // 避免重复触发:只有值真的变化才触发更新
      if (!hadKey || !Object.is(oldValue, value)) {
        trigger(target, 'set', key, value, oldValue); // 核心:触发更新
      }

      return result;
    },

    // 拦截删除操作(如 delete obj.xxx)
    deleteProperty(target, key) {
      const hadKey = hasOwn(target, key);
      const result = Reflect.deleteProperty(target, key);
      // 只有删除成功才触发更新
      if (hadKey && result) {
        trigger(target, 'delete', key);
      }
      return result;
    },

    // 拦截 in 操作(如 'xxx' in obj)
    has(target, key) {
      const result = Reflect.has(target, key);
      // 收集依赖
      if (!isSymbol(key)) {
        track(target, 'has', key);
      }
      return result;
    },

    // 拦截 Object.keys/for...in 等遍历操作
    ownKeys(target) {
      track(target, 'iterate', Array.isArray(target) ? 'length' : ITERATE_KEY);
      return Reflect.ownKeys(target);
    }
  });

  // 缓存代理对象,避免重复创建
  reactiveMap.set(target, proxy);
  return proxy;
}

3. 依赖收集(track)与触发更新(trigger)核心逻辑

tracktrigger 是响应式的灵魂,简化版逻辑如下:

// 存储依赖的映射表:target -> key -> 副作用函数集合
const targetMap = new WeakMap();
// 当前正在执行的副作用函数(如组件渲染函数、watch回调)
let activeEffect = null;

// 收集依赖
function track(target, type, key) {
  // 无活跃副作用函数时,无需收集
  if (!activeEffect) return;
  
  // 1. 获取target对应的依赖映射
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()));
  }
  
  // 2. 获取key对应的副作用函数集合
  let dep = depsMap.get(key);
  if (!dep) {
    depsMap.set(key, (dep = new Set()));
  }
  
  // 3. 将当前副作用函数加入集合
  if (!dep.has(activeEffect)) {
    dep.add(activeEffect);
    // 反向收集:让副作用函数记住自己被哪些dep收集,用于清理
    activeEffect.deps.push(dep);
  }
}

// 触发更新
function trigger(target, type, key, newValue?, oldValue?) {
  // 1. 获取target对应的依赖映射
  const depsMap = targetMap.get(target);
  if (!depsMap) return;
  
  // 2. 收集所有需要执行的副作用函数
  const effects = new Set();
  const add = (effectsToAdd) => {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect => effects.add(effect));
    }
  };
  
  // 3. 根据操作类型获取对应的副作用函数
  if (key !== void 0) {
    add(depsMap.get(key)); // 触发key对应的副作用
  }
  
  // 4. 执行所有副作用函数
  effects.forEach(effect => {
    if (effect.scheduler) {
      effect.scheduler(); // 调度执行(如nextTick)
    } else {
      effect(); // 直接执行
    }
  });
}

三、关键细节解析

1. 为什么用 Proxy 而不是 Object.defineProperty?

  • 支持数组:Proxy 能拦截数组的 push/pop/splice 等操作(通过 set 拦截),而 Object.defineProperty 无法监听数组索引变化。
  • 支持新增 / 删除属性:Proxy 能拦截 deleteProperty 和新增属性的 set 操作,而 Object.defineProperty 只能监听已有属性。
  • 支持 Map/Set 等集合类型:Proxy 可以拦截这些内置对象的方法(如 set/delete)。

2. 缓存机制(reactiveMap)

reactiveMap 是一个 WeakMap(弱引用,不影响垃圾回收),用于存储「原始对象 -> 代理对象」的映射,避免对同一个对象重复创建 Proxy

const obj = {};
const r1 = reactive(obj);
const r2 = reactive(obj);
console.log(r1 === r2); // true

3. 深度响应式

get 拦截器中,若读取的属性值是对象,则递归调用 reactive 转为响应式,实现深度代理

4. 避免无限递归

通过 ReactiveFlags.RAW 标记原始对象,当代理对象的 get 操作读取 __v_raw 时,直接返回原始对象,避免代理对象访问自身导致的无限递归。

5. 边界处理

  • 非对象类型(如字符串、数字)直接返回,不代理;
  • 只读对象(readonly 创建的)不重复代理;
  • 只有值真正变化时(!Object.is(oldValue, value))才触发更新,避免无效更新。

四、总结

  1. reactive 核心是通过 Proxy 拦截对象操作,在读取时 track 收集依赖,修改时 trigger 触发更新;
  2. 利用 WeakMap(reactiveMap/targetMap)实现缓存和依赖管理,兼顾性能和垃圾回收;
  3. 相比 Vue2 的响应式,reactive 支持数组、新增属性、复杂集合类型,且实现更简洁。

补充:与 ref 的区别

  • reactive 用于对象 / 数组的响应式,基于 Proxy 实现;
  • ref 用于基本类型(如字符串、数字)的响应式,本质是封装成 { value: xxx } 的对象,再通过 reactive 代理 value 属性。