Proxy代理在前端开发中的实战应用与常见陷阱解析

上官玉楠 前端 阅读 2,190
赞 21 收藏
二维码
手机扫码查看
反馈

Proxy代理改了对象,结果Vue响应式失效了?

今天下午三点十七分,我一边喝着第三杯速溶咖啡,一边盯着控制台里那个死活不更新的页面发呆——明明数据变了,视图就是不重渲染。最后发现是 Proxy 搞的鬼,不是 Vue 的锅,也不是我的逻辑错了,而是我把 Proxy 用在了一个它本不该出现的地方。

Proxy代理在前端开发中的实战应用与常见陷阱解析

事情起因很简单:我在写一个「表单字段级权限控制」功能,需要根据后端返回的权限配置,动态屏蔽某些字段(比如隐藏手机号、禁用提交按钮)。一开始我直接改 state 对象,但发现要手动处理太多嵌套层级,于是脑子一热,想用 Proxy 给整个表单数据包一层拦截,让 get/set 自动过滤掉无权访问的 key。

代码大概长这样:

const formData = { name: '张三', phone: '138xxxx1234', email: 'zhang@example.com' };
const permissions = { name: true, phone: false, email: true };

const proxiedData = new Proxy(formData, {
  get(target, key) {
    if (permissions[key] === false) return undefined;
    return target[key];
  },
  set(target, key, value) {
    if (permissions[key] !== false) {
      target[key] = value;
      return true;
    }
    return true; // 静默失败
  }
});

然后我把它塞进了 Vue 3 的 ref 里:

import { ref } from 'vue';
const form = ref(proxiedData);

结果你猜怎么着?form.value.name 能读,form.value.phoneundefined(符合预期),但当我执行 form.value.name = '李四',界面纹丝不动。DevTools 里看到 form 的响应式依赖根本没被收集,effect 没触发,trigger 更是压根没跑。

这里我踩了个坑:以为 Proxy + ref 就能无缝接入 Vue 响应式系统。实际上,Vue 3 的响应式是基于 reactive() 内部对原始对象做 Proxy 拦截实现的,但它只认自己创建的 Proxy,不认你手写的。

我第一反应是“是不是 ref 不支持 Proxy?”——试了下,ref(new Proxy({...}, {...})) 确实不会触发依赖收集,因为 ref 只是对值做一层包装,它内部不会去递归分析你传进来的对象是不是 Proxy,更不会帮你把它的 get/set 接入自己的 track/trigger 流程。

后来试了下发现,如果我把 proxiedData 直接传给 reactive(),也一样挂——reactive({ ...proxiedData }) 行不通,因为 reactive 会先 isPlainObject 判断,而 Proxy 实例过不了这个检测,直接 fallback 到 shallowReactive 或者干脆不响应。

折腾了半天,翻了 Vue 源码(packages/reactivity/src/reactive.ts),确认了一点:Vue 的响应式系统只对 plain object / array / Map / Set 等内置类型做深度代理,不接管用户自定义 Proxy。这是设计使然,不是 bug。

所以问题本质不是 Proxy 不能用,而是我把它和 Vue 响应式混用了——两个 Proxy 在打架。

解决方案就三行,但我想了两小时

其实最简单的办法,根本不用 Proxy。

我重新捋了需求:我要的是「读的时候过滤字段,写的时候限制字段」,而不是「实时劫持所有操作」。那干嘛不直接封装一个访问器对象?

最终我写了这么个轻量 wrapper:

class FormAccessor {
  constructor(data, permissions) {
    this._data = data;
    this._perms = permissions;
  }

  get(key) {
    return this._perms[key] !== false ? this._data[key] : undefined;
  }

  set(key, value) {
    if (this._perms[key] !== false) {
      this._data[key] = value;
      return true;
    }
    return false;
  }

  toJSON() {
    const obj = {};
    for (const key in this._data) {
      if (this._perms[key] !== false) {
        obj[key] = this._data[key];
      }
    }
    return obj;
  }
}

然后在 Vue 组件里这么用:

import { reactive, watch } from 'vue';

const rawData = reactive({ name: '张三', phone: '138xxxx1234', email: 'zhang@example.com' });
const permissions = { name: true, phone: false, email: true };
const form = new FormAccessor(rawData, permissions);

// 手动触发更新(可选)
watch(() => JSON.stringify(rawData), () => {
  // 如果你需要响应式地暴露 form.toJSON(),可以在这里 emit 或更新 computed
});

但这样还不够爽。我又加了个 computed 包装:

import { computed, reactive } from 'vue';

const rawData = reactive({ name: '张三', phone: '138xxxx1234', email: 'zhang@example.com' });
const permissions = { name: true, phone: false, email: true };

const visibleForm = computed(() => {
  const obj = {};
  for (const key in rawData) {
    if (permissions[key] !== false) {
      obj[key] = rawData[key];
    }
  }
  return obj;
});

模板里直接用 {{ visibleForm.name }},完美响应,没有副作用,也不污染原始数据结构。

不过这里有个小尾巴:如果权限是异步加载的(比如从 fetch('https://jztheme.com/api/permissions') 拿),那 computed 里的 permissions 得是 reactive 的,否则依赖不会自动更新。所以我后来改成:

const permissions = reactive({
  name: null,
  phone: null,
  email: null
});

// 权限加载完后:
fetch('https://jztheme.com/api/permissions')
  .then(res => res.json())
  .then(data => {
    Object.assign(permissions, data); // 触发 computed 重计算
  });

现在 visibleForm 能随权限动态变化了。虽然没用 Proxy 那么“酷”,但稳定、可测、易 debug,上线后没再出过问题。

当然,如果你真想用 Proxy 和 Vue 共存,也不是完全不行。你可以把 Proxy 套在非响应式数据上,再通过 triggerRefmarkRaw 显式控制更新时机。但我试了两次,都因为边界 case(比如数组 length 变化、新增 key)导致漏更新,最后还是放弃了。

Proxy 真正适合干啥?

回头想想,Proxy 最适合的场景其实是:你不关心框架响应式,只想拦截原始操作行为。

比如我上周写的请求参数校验中间件:

function createValidatedAPI(baseURL) {
  return new Proxy({}, {
    get(_, prop) {
      return async (data) => {
        if (!data || typeof data !== 'object') throw new Error('data must be object');
        const res = await fetch(${baseURL}/${prop}, {
          method: 'POST',
          body: JSON.stringify(data)
        });
        return res.json();
      };
    }
  });
}

const api = createValidatedAPI('https://jztheme.com/api');
api.submitOrder({ orderId: 123 }); // 自动校验

这种纯逻辑拦截,不和框架耦合,Proxy 就很舒服。一旦和 Vue、React 这类声明式 UI 框架的响应式系统交叉,就得格外小心——它们都有自己的代理层,硬塞进去容易互相覆盖。

以上是我踩坑后的总结,希望对你有帮助。如果你有更好的方案,比如用 shallowRef + 自定义 effect 管理,或者用 proxyRefs 巧妙桥接,欢迎评论区交流。这个技巧的拓展用法还有很多,后续会继续分享这类博客。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论