一、为什么需要手写响应式系统

作为前端开发者,我们每天都在用 Vue3 的 reactiveref,它们让数据变化自动触发视图更新,简直不要太方便。但你知道它们底层是怎么运作的吗?手写实现一遍不仅能加深理解,还能在面试时惊艳面试官,甚至在某些需要高度定制响应式逻辑的场景派上用场。

举个例子,假设我们要实现一个简单的数据绑定:

// 技术栈:Vue3 + Composition API
const state = reactive({ count: 0 });
watchEffect(() => {
  console.log(`count变化了:${state.count}`);
});
state.count++; // 控制台输出:count变化了:1

看起来很简单,但 reactive 背后其实依赖了 JavaScript 的 Proxy 和依赖收集机制。接下来,我们就一步步拆解它。

二、实现简易版 reactive

1. Proxy 的基本使用

Proxy 是 ES6 提供的拦截器,可以监听对象的读写操作。我们先写个最简单的拦截:

function reactive(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      console.log(`读取了属性 ${key}`);
      return Reflect.get(target, key, receiver);
    },
    set(target, key, value, receiver) {
      console.log(`设置了属性 ${key} 为 ${value}`);
      return Reflect.set(target, key, value, receiver);
    },
  });
}

const obj = reactive({ foo: 1 });
obj.foo; // 控制台输出:读取了属性 foo
obj.foo = 2; // 控制台输出:设置了属性 foo 为 2

2. 依赖收集与触发

Vue3 通过 tracktrigger 实现依赖收集。我们模拟一个简化版:

const targetMap = new WeakMap(); // 存储所有响应式对象的依赖关系
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 deps = depsMap.get(key);
  if (!deps) {
    deps = new Set();
    depsMap.set(key, deps);
  }
  deps.add(activeEffect); // 收集当前副作用函数
}

function trigger(target, key) {
  const depsMap = targetMap.get(target);
  if (!depsMap) return;
  const deps = depsMap.get(key);
  if (deps) {
    deps.forEach(effect => effect()); // 触发所有依赖该属性的副作用函数
  }
}

function effect(fn) {
  activeEffect = fn;
  fn(); // 首次执行,触发依赖收集
  activeEffect = null;
}

3. 整合 Proxy 与依赖收集

现在把 tracktrigger 整合到 reactive 中:

function reactive(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      track(target, key); // 读取时收集依赖
      return Reflect.get(target, key, receiver);
    },
    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;
    },
  });
}

// 测试代码
const state = reactive({ count: 0 });
effect(() => {
  console.log(`count更新了:${state.count}`);
});
state.count++; // 输出:count更新了:1

三、实现简易版 ref

ref 的本质是对原始值的包装,使其也能被响应式追踪。我们来实现一个:

function ref(value) {
  return reactive({
    value,
  });
}

// 测试代码
const num = ref(0);
effect(() => {
  console.log(`num.value变化了:${num.value}`);
});
num.value++; // 输出:num.value变化了:1

不过 Vue3 的 ref 实现更高效,直接基于 getter/setter

function ref(value) {
  const wrapper = {
    get value() {
      track(wrapper, 'value');
      return value;
    },
    set value(newValue) {
      if (value !== newValue) {
        value = newValue;
        trigger(wrapper, 'value');
      }
    },
  };
  return wrapper;
}

四、应用场景与技术细节

1. 应用场景

  • 自定义响应式逻辑:比如对某些字段的修改增加额外校验。
  • 性能优化:手动控制依赖收集范围,避免不必要的更新。
  • 库开发:构建自己的状态管理工具时,可能需要定制响应式行为。

2. 技术优缺点

优点

  • 灵活性高,可以按需定制。
  • 深入理解 Vue3 响应式原理,便于排查问题。

缺点

  • 手写实现可能遗漏边界情况(如数组方法拦截)。
  • 生产环境建议直接使用 Vue3 官方 API。

3. 注意事项

  • 循环引用Proxy 不会自动处理循环引用,需额外处理。
  • 性能:频繁的 track/trigger 可能成为性能瓶颈。
  • 兼容性Proxy 无法被 polyfill,低版本浏览器需降级。

五、总结

通过手写 reactiveref,我们揭开了 Vue3 响应式系统的神秘面纱。虽然实际开发中直接使用官方 API 更稳妥,但理解底层机制能让你在复杂场景下游刃有余。下次遇到响应式问题,不妨想想 Proxy 和依赖收集,或许能更快找到解决方案!