Skip to content

proxy简介

只做简到不能再简的简介,详细用法请查阅MDN阮一峰ES6

proxy 一般用于库的封装设计,非必要不建议用在业务代码里,因为它会彻底改变数据的行为,很反常识

js
let obj = {
  a: "1",
};

let proxy = new Proxy(obj, {
  get(obj, key) {
    console.log(obj, key);
    return obj[key];
  },
  defineProperty() {
    console.log(arguments);
  },
});

控制台执行

  • obj.a 输出 1 不打印
  • proxy.a 打印内容并输出 1
  • Object.defineProperty(proxy, "a", {value:10}) 打印函数的参数

这里可以看出原对象的 get 不会走进 proxy 因此,我们也可以把 proxy 不理解成代理,而是当作一个完全自定义的对象或者类或者工具库

proxy 的使用方式和 promise 类似

js
function a() {
  return new Promise();
}

function b() {
  return new Proxy();
}

很少像示例那样直接 new proxy 后赋值给个变量

数据双向绑定示例

React

js
// 数据
const [data, setData] = useState({ count: 1 }); // reactive()

// 观测变化 需要手动声明依赖(手动声明这个回调对应什么的ey)
useEffect(() => console.log("count changed", data.count), [data.count]);

// 触发 console.log('count changed', data.count) 重新执行
// 手动通过工具触发修改值,而不是直接修改
setData({
  count: 2,
});

Vue3

js
const data = reactive({ count: 0 }); // 定义一个代理后的数据

effect(() => console.log("count changed", data.count)); // 定义回调,代理数据变化时触发

data.count = 2; // 修改代理后的值

👆 我们可以看到 vue3 相比于 vue2,需要手动把期望响应式的数据放到 reactive

vue3 相比于 reactive ,又简化了很多步骤,甚至像个无法理解的黑盒

基于Proxy实现数据双向绑定

需要实现的效果如👇

js
let dummy; // 假数据
const counter = reactive({ num: 0 }); // 定义一个代理后的数据
effect(() => (dummy = counter.num)); // 定义回调,代理数据变化时触发
// dummy = counter.num; // 直接赋值的形式不会双向绑定

console.log(dummy); // 0
counter.num = 7; // 修改代理后的值
console.log(dummy); // 7

1. 实现基础get、set代理

js
(function () {
  let dummy;

  function reactive(data) {
    return new Proxy(data, {
      get(originData, key) {
        console.log("触发get", key);
        return originData[key];
      },
      set(originData, key, newVal) {
        console.log("触发set", key);
        return (originData[key] = newVal);
      },
    });
  }
  const myData = { num: 1 }; // 原数据
  const proxyData = reactive(myData); // 代理后数据
  // 因为代理的get和set处理好了 修改原对象和修改代理对象 再访问原对象或者代理对象都能是新的值
  dummy = proxyData.num;
  console.log(dummy); // 1
  proxyData.num = 2;
  console.log(dummy); // 1
})();

👆 代理后的数据赋值给 dummydummy 不会随着代理数据变化而变化

但是使用数据的时候使用代理对象不就好了吗,为什么要赋值出来使用 看起操作的数据是外层的,不用关心数据是不是在代理对象里层?

2. set时触发订阅者的回调

要实现修改代理对象数据,能通知到赋值的变量,也就是 set 的时候再触发一次赋值(回调) 因此赋值操作需要立即执行一次,以及传递到 set 的回调时执行一次,用 effect 封装起来

js
(function () {
  let dummy;
  const callbackList = [];

  function reactive(data) {
    return new Proxy(data, {
      get(originData, key) {
        console.log("触发get", key);
        return originData[key];
      },
      set(originData, key, newVal) {
        console.log("触发set", key);
        originData[key] = newVal;
        callbackList.forEach((fn) => fn());
        return newVal;
      },
    });
  }
  const myData = { num: 1 }; // 原数据
  const proxyData = reactive(myData); // 代理后数据

  function effect(callback) {
    callback(); // 1. 立即执行一次
    callbackList.push(callback); // 2. 传递给set的时候执行一次
    // 因为代理整个对象所以需要多个不同观察者,存储层数组,在set的时候全部执行(性能浪费)
  }

  effect(() => (dummy = proxyData.num)); // 触发立即执行回调中赋值的get
  console.log(dummy); // 1 不触发proxy的get set
  proxyData.num = 2; // 触发set 和回调中的赋值的get
  console.log(dummy); // 2 不触发proxy的get set
  /* effect(()=>{
      console.log('触发num的回调');
      dummy = proxyData.num
  }); // 触发立即执行回调中赋值的get
  effect( ()=>{
      console.log('触发num2的回调')
      dummy2 = proxyData.num2
  } ); // 触发立即执行回调中赋值的get
  console.log(dummy); // 1 不触发proxy的get set
  proxyData.num = 2; // 触发set 和回调中的赋值的get
  proxyData.num2 = 2; // 触发set 和回调中的赋值的get
  console.log(dummy); // 2 不触发proxy的get set*/
})();

👆 假如代理多个 reactive 对象,每个对象中多个属性,都共用一个 callback 列表,并且在 set 时触发所有的回调,性能浪费

理想状态是,set 具体的 reactive 对象的具体 key 触发相应的回调

3. set匹配到相应的回调来触发

我们看到现在收集回调的地方,是在立即执行时存起来,只要想办法不是单纯的往回调列表中 push 而是存储对象 key value,即可在 set 的时候调用相应的回调 但是回调函数是没有明显的 key 给我们定义的

因为要定位到是哪个 reactive 对象和 key,因此理想的回调map数据结构如下

js
const callbackMap = new Map([
  [ originObj1, new Map([ [key1: callbackList], [key2: callbackList] ...  ])]
  [ originObj2, new Map([ [key1: callbackList], [key2: callbackList] ...  ])]
])

我们再看一下整个流程,不想污染外部,又不想在外部额外调用工具来存储回调,发现只有 proxy 内部可以获取到 originObjkey

那我们利用立即执行回调时,触发 get 的时机,存储回调 (有点类似 vue2 的 computed,依赖到的变量才会触发收集) 在立即执行触发的 get 里,可以初始化出回调 map 的数据结构 但是回调函数在 effect 里,此时没有 originObjkey ,即使已经初始化好回调 map 数据结构,也无法往对应的地方存储回调函数

所以外加一个临时变量存储立即执行触发的 get 对应的回调 map 数据结构中的回调列表,这样在 effect 里就可以往临时变量里存回调了 而在 set 的时候根据 key 获取回调 map 中对应的回调执行即可

js
(function () {
  let dummy;
  const callbackMap = new Map();
  let tempCallbackList = []; // 临时变量 立即执行的回调之后获取的对应的回调列表

  function reactive(data) {
    return new Proxy(data, {
      get(originData, key) {
        console.log("触发get", key);
        if (!callbackMap.get(originData)) {
          callbackMap.set(originData, new Map());
        }
        if (!callbackMap.get(originData).get(key)) {
          callbackMap.get(originData).set(key, []);
        }
        tempCallbackList.push(callbackMap.get(originData).get(key));
        return originData[key];
      },
      set(originData, key, newVal) {
        console.log("触发set", key);
        originData[key] = newVal;
        callbackMap
          .get(originData)
          .get(key)
          .forEach((fn) => fn());
        return newVal;
      },
    });
  }
  const myData = { num: 1 }; // 原数据
  const proxyData = reactive(myData); // 代理后数据

  function effect(callback) {
    tempCallbackList = [];
    callback(); // 1. 立即执行一次
    tempCallbackList.forEach((list) => {
      list.push(callback);
    });
  }

  effect(() => (dummy = proxyData.num)); // 触发立即执行回调中赋值的get
  console.log(dummy); // 1 不触发proxy的get set
  proxyData.num = 2; // 触发set 和回调中的赋值的get
  console.log(dummy); // 2 不触发proxy的get set
  /*effect(()=>{
      console.log('触发num的回调');
      dummy = proxyData.num
  }); // 触发立即执行回调中赋值的get
  effect( ()=>{
      console.log('触发num2的回调')
      dummy2 = proxyData.num2
  } ); // 触发立即执行回调中赋值的get
  console.log(dummy); // 1 不触发proxy的get set
  proxyData.num = 2; // 触发set 和回调中的赋值的get
  proxyData.num2 = 2; // 触发set 和回调中的赋值的get
  console.log(dummy); // 2 不触发proxy的get set*/
})();

👆 在我们测试使用多个 effect 时没有问题,每个 effect 都会往对应的 key 里存储回调 但是当一个 effect 里依赖多个变量时发现了问题,只把回调存储到了最后一个变量的回调里 这是因为临时变量只对应一个 key 的回调列表 所以临时变量应该对应多个 key 的回调函数列表 在存储回调的时候遍历临时变量列表来存储

到这里,已经可以有 vue3 双向绑定数据的雏形 也可以发现我们是通过 get 来做回调存储的 当不触发 get 的时候就不会有回调 如:使用判断语句,只有在运行时才能知道会不会进 get ,在 effect 阶段不知道

4. 深度proxy

另外,proxy 并不自动支持深度代理, key 对应的回调也不没有深度数据

要想实现深度数据的 key,首先想到的可能是递归,但是这里有个更取巧的方法 就是, effect 立即执行阶段,如果是深度数据,只会触发外层的 proxy 我们可以利用判断当前值是对象时返回一个 proxy 代理,让深度的值继续触发 get,形成递归的样子

js
(function () {
  let dummy;
  const callbackMap = new Map();
  let tempCallbackList = []; // 临时变量 立即执行的回调之后获取的对应的回调列表

  function reactive(data) {
    return new Proxy(data, {
      get(originData, key) {
        console.log("触发get", key);
        if (!callbackMap.get(originData)) {
          callbackMap.set(originData, new Map());
        }
        if (!callbackMap.get(originData).get(key)) {
          callbackMap.get(originData).set(key, []);
        }
        tempCallbackList.push(callbackMap.get(originData).get(key));
        // 判断当前值是对象时继续代理,形成递归
        if (typeof originData[key] === "object") {
          return reactive(originData[key]);
        }
        return originData[key];
      },
      set(originData, key, newVal) {
        console.log("触发set", key);
        originData[key] = newVal;
        callbackMap
          .get(originData)
          .get(key)
          .forEach((fn) => fn());
        return newVal;
      },
    });
  }
  const myData = { a: { b: 1 } }; // 原数据
  const proxyData = reactive(myData); // 代理后数据

  effect(() => {
    dummy = proxyData.a.b;
  });
  console.log(dummy);
  proxyData.a.b = 2;
  console.log(dummy);
})();

这样就让深度的对象类型数据也走一次 proxy 但是这样的写法有点性能问题,每次深度取值都要重新走一次 proxy 即使同一个值已经走过一次

5. 优化深度代理

这里我们用一个 map 存储深度对象的 proxy ,就以对象为 key 其实也像是把深度对象扁平化出来存储了,仅仅为了省去每次深度取值时的重复 proxy

js
const deepProxyMap = new Map(); // 其实也像是把深度对象扁平化出来存储了,仅仅为了省去每次深度取值时的重复

function reactive(data) {
  if (deepProxyMap.has(data)) {
    return deepProxyMap.get(data);
  }

  const proxy = new Proxy(data, {
    get(originData, key) {
      console.log("触发get", key);
      if (!callbackMap.get(originData)) {
        callbackMap.set(originData, new Map());
      }
      if (!callbackMap.get(originData).get(key)) {
        callbackMap.get(originData).set(key, []);
      }
      tempCallbackList.push(callbackMap.get(originData).get(key));
      if (typeof originData[key] === "object") {
        return reactive(originData[key]);
      }
      return originData[key];
    },
  });
  deepProxyMap.set(data);
  return proxy;
}

结合dom的reactive案例

1. input双向数据绑定(v-model)

2. 拖拽取色值


目前很流行的一种大型项目管理方式 Monorepo

参考资料

proxy-MDN

vue3-reactivity

Vue3 的 Proxy 能做到哪些精确的拦截操作?原理揭秘