前端开发··2 阅读·预计 9 分钟

响应式状态演进:从 JavaScript Proxy 到 React 与 Vue 的设计博弈

现代前端框架的核心在于状态与视图的同步。Vue 与 React 走向了截然不同的道路:Vue 依赖 JavaScript 的 Proxy 实现细粒度响应式,而 React 则坚守不可变数据驱动粗粒度调和。理解这层底层设计,是写出高性能代码的关键。

JavaScript Proxy:拦截与代理的原生引擎

Vue3 放弃 Object.defineProperty,全面拥抱 Proxy,根本在于 Proxy 能拦截对象的所有操作,而非仅仅属性的读写。

反例:传统对象属性修改无感知

const state = { count: 0 };
state.count++; // 视图如何得知?需要手动触发更新或脏检查

正例:利用 Proxy 构建最小响应式内核

const reactive = (obj) => {
  const deps = new Map();
  
  return new Proxy(obj, {
    get(target, key) {
      if (!deps.has(key)) deps.set(key, new Set());
      // 依赖收集:假设 activeEffect 为当前运行的副作用函数
      if (activeEffect) deps.get(key).add(activeEffect);
      return Reflect.get(target, key);
    },
    set(target, key, value) {
      const result = Reflect.set(target, key, value);
      // 派发更新
      deps.get(key)?.forEach(effect => effect());
      return result;
    }
  });
};

let activeEffect;
const effect = (fn) => { activeEffect = fn; fn(); activeEffect = null; };

const state = reactive({ count: 0 });
effect(() => console.log('视图更新:', state.count)); // 初始执行: 视图更新: 0
state.count++; // 自动触发: 视图更新: 1

Vue3:基于 Proxy 的细粒度依赖收集

Vue 利用 Proxy 拦截 getter 收集依赖,拦截 setter 触发更新,实现精准渲染。但 Proxy 的代理是针对对象的,基本类型无法拦截,这也是 ref 存在的意义。

反例:错误解构导致响应式丢失

import { reactive } from 'vue';

const state = reactive({ user: { name: 'Alice' } });
// 解构取值脱离了 Proxy 的 get 拦截环境
const { name } = state.user; 
name = 'Bob'; // 视图无更新,name 只是一个普通字符串变量

正例:保持 Proxy 链路或使用 toRefs

import { reactive, toRefs } from 'vue';

const state = reactive({ user: { name: 'Alice' } });

// 1. 通过 Proxy 对象访问
state.user.name = 'Bob'; // 触发 setter,视图更新

// 2. 使用 toRefs 为属性创建 ref 包装
const { name } = toRefs(state.user);
name.value = 'Charlie'; // 触发 ref 的 setter,视图更新

React:拥抱不可变数据的粗粒度调和

React 并不追踪属性的读写,而是通过比对前后状态引用是否相同(Object.is)来决定是否重渲染。修改同一份引用,React 将忽略更新。

反例:直接变异(Mutate)State

const [list, setList] = useState([1, 2, 3]);

const addItem = () => {
  list.push(4);    // 直接修改原引用
  setList(list);   // 引用未变,React 浅比较判定无变化,拒绝重渲染
};

正例:产生新的引用触发更新

const [list, setList] = useState([1, 2, 3]);

const addItem = () => {
  setList([...list, 4]); // 扩展运算符生成新数组,引用改变,触发更新
};

跨框架状态设计的统一范式

当我们在大型应用中需要设计脱离框架层面的通用状态库时,如何同时兼容 Vue 的响应式与 React 的不可变?核心思路是:底层使用 Proxy 监听写操作,但在面向 React 消费时,包装为不可变更新接口。

跨框架状态管理核心实现:

class Store {
  #state;
  #listeners = new Set();

  constructor(initialState) {
    // Vue 可直接消费此 reactive 对象
    this.#state = reactive(initialState); 
  }

  // 适用于 React 的不可变更新钩子
  useReactState() {
    const [, setState] = useState(0);
    
    useEffect(() => {
      const triggerRender = () => setState(prev => prev + 1);
      // 当 Proxy set 派发更新时,触发 React 重渲染
      this.#listeners.add(triggerRender);
      return () => this.#listeners.delete(triggerRender);
    }, []);

    // 返回快照,切断直接修改的可能,符合 React 不可变理念
    const snapshot = { ...this.#state };
    return snapshot;
  }

  // 统一修改入口
  mutate(updater) {
    updater(this.#state); // 触发 Proxy set
    this.#listeners.forEach(fn => fn()); // 通知 React
  }
}

// 使用
const store = new Store({ count: 0 });
store.mutate(state => state.count++); // Vue 自动响应,React 触发重渲染

结语

Vue 的 Proxy 是对 JavaScript 引擎特性的极致压榨,以自动依赖追踪换取开发便利;React 的不可变是对数据流动的强约束,以逻辑可预测性换取并发渲染的安全。理解 Proxy 的边界与不可变数据的威力,才能在框架的规则下游刃有余。

0 评论

评论区

登录 后参与评论