Vue3源码 - 手动实现一个简单的Reactive

46 阅读4分钟

前言

最近在学习 Vue 3 响应式系统,记录一下动手写的一个极简版 reactive 实现。这个实现没有额外添加任何功能,目的就是把 Vue 3 中 reactive 的核心设计思路用最简洁的方式抽出来。

设计思路

一个可靠的 reactive 函数必须解决以下几个关键问题:

  1. 如何判断一个对象是否已经被代理过? 如果用户多次对同一个对象调用 reactive,必须返回同一个代理对象,否则依赖关系会丢失,导致响应式失效。

  2. 如何避免重复创建 Proxy? 创建 Proxy 是有开销的,而且同一个原始对象只能对应一个代理实例。最好的方式是用一个缓存结构来记录“原始对象 → 代理对象”的映射。

  3. 用什么数据结构做缓存? 键必须是原始对象本身,且当原始对象不再被引用时,缓存应该自动清除 → 自然想到用 WeakMap。

  4. 如何快速判断一个对象是否是 reactive 创建的(实现 isReactive)? 最简单的办法是在 Proxy 的 get 拦截器里,专门处理一个“魔法 key”。当有人访问这个 key 时,直接返回 true,表示我是 reactive 对象。这样既不需要修改原始对象,又能在运行时快速判断。

  5. 非对象类型怎么处理? reactive 只针对对象生效,如果传入原始值,直接返回。

基于以上思路,设计核心就是:

  • 用 WeakMap 缓存代理结果
  • 用一个特殊的标志 key(__v_isReactive)在 get 中返回 true 来支持判断
  • 在创建前先检查标志 + 缓存,双重保险防止重复代理

代码实现

第一步:定义标志位和 Proxy 处理函数

// 建立枚举,定义一个特殊的标志 key
export enum ReactiveFlags {
  IS_REACTIVE = '__v_isReactive'
}

// Proxy 的 get/set 拦截器
export const mutableHandlers: ProxyHandler<any> = {
  /**
   * @param target - 原始对象
   * @param key - 访问的属性
   * @param receiver - 代理对象本身(Proxy 实例)
   */
  get(target, key, receiver){
    // 当访问这个特殊 key 时,直接返回 true,这就是我们判断 isReactive 的依据
    if(key === ReactiveFlags.IS_REACTIVE){
      return true;
    }
    
    // 使用 Reflect.get 正确转发操作
    return Reflect.get(target, key, receiver)
  },
  
  set(target, key, value, receiver){
  
    // 使用 Reflect.set 正确设置值
    return Reflect.set(target, key, value, receiver);
  },
}

这里通过 get 拦截实现 isReactive 判断,并且这里必须使用Reflect,而不是return target[key]或者是receiver[key]

const obj = {
  _name: 'Vue',
  get name() {
    return this._name.toUpperCase();
  }
};

const proxy = new Proxy(obj, {
  get(target, key, receiver) {
    // return target[key]; // 错误方式,这导致访问proxy.name时,在this._name时,由于this指向了target,而没有指向proxy,所以proxy里的get无法拦截到_name的调用
    // return receiver[key] // 错误方式,会导致无限进入get循环
    return Reflect.get(target, key, receiver);  // 正确方式
  }
});

console.log(proxy.name); // 期望输出 "VUE"

第二步:实现 reactive 函数(reactive.ts)

import { isObject } from "@vue/shared";  // Vue 官方的工具函数,判断是否为对象(排除 null)
import { ReactiveFlags, mutableHandlers } from "./baseHandler";

// 用 WeakMap 缓存:原始对象 => 代理对象
// 弱引用,自动垃圾回收,不泄漏内存
const reactiveMap = new WeakMap()

// 对外暴露的 reactive 函数
export function reactive(target){
  return createReactiveObject(target);
}

// 核心创建逻辑
function createReactiveObject(target){
  // 步骤 1:如果不是对象,直接返回(当前返回 undefined)
  if(!isObject(target)){
   return;
  }
  
  // 步骤 2:如果目标对象已经带有标志,说明它已经被代理过了
  // 注意:这里访问 target[flag] 会触发 Proxy 的 get,返回 true
  // 即使传入的是 proxy 本身,也能正确识别
  if(target[ReactiveFlags.IS_REACTIVE]){
   return target;
  }
  
  // 步骤 3:检查缓存中是否已经存在这个原始对象的代理
  if(reactiveMap.has(target)){
   return reactiveMap.get(target);
  }
  
  // 步骤 4:真正创建 Proxy 代理
  const proxy = new Proxy(target, mutableHandlers);
  
  // 步骤 5:缓存起来,下次直接复用同一个 proxy
  reactiveMap.set(target, proxy);
  return proxy;
}

完整流程

  1. 调用 reactive(obj)
  2. 先判断 obj 是否是对象 → 不是则返回
  3. 访问 obj[__v_isReactive] → 如果是已代理对象,会触发 get 返回 true → 直接返回自身
  4. 检查 reactiveMap 是否已有缓存 → 有则直接返回
  5. 创建 new Proxy(obj, mutableHandlers)
  6. 把映射关系存入 reactiveMap
  7. 返回 proxy

测试效果

const raw = { count: 0 };
const proxy1 = reactive(raw);
const proxy2 = reactive(raw);        // 缓存命中
const proxy3 = reactive(proxy1);     // 标志位判断命中

console.log(proxy1 === proxy2); // true
console.log(proxy1 === proxy3); // true
console.log(proxy1[ReactiveFlags.IS_REACTIVE]); // true

实现了“同一个对象只代理一次,返回同一个代理实例”。

总结

  • WeakMap 缓存避免重复代理
  • 特殊标志 key + get 拦截实现 isReactive 判断
  • 不污染原始对象
  • 支持传入 proxy 时直接返回自身