Vue3如何使用Proxy实现代理

简介: Vue3如何使用Proxy实现代理

首先来介绍一下Proxy

Proxy

它的第一个参数接收的是一个被代理对象target,第二个参数是一个配置对象,包含一组捕获器handle,将会返回一个代理对象ProxyTarget.

这些handle定义了在ProxyTarget上触发的各种基本操作的行为.每个handle都是一个函数,用来处理相应的操作

例如:
读取ProxyTarget中的属性.
读取(get)是基本操作的一种,所以它将被get捕获器拦截.并且在get函数内可以重新定义返回结果.target是被代理对象,key是访问的属性.
因为 重新定义了返回结果return target[key];
访问ProxyTarget中的属性就等于直接访问了被代理对象的属性一样.

const ProxyTarget = new Proxy(target, {
   
  get(target, key) {
   
    return target[key];
  },
});

Reflect

它是一个全局方法,其中有

  • reflect.get
  • reflect.set等

任何在 Proxy 的拦截器中能够找到的方法,都能够在 Reflect 中找到同名函数,它们的操作等价.不同的是
Reflect函数还能接收一个参数,即指定接收者 receiver,你可以把它理解为函数调用过程中的 this

既然操作等价,为什么Vue使用的是Reflect下的函数呢.举个例子

对下面的对象进行代理: 当我们使用 proxyTarget.bar 访问 bar属性时,它的 getter 函数内的 this 指向的其实是原始对象 target,这说明我们最终访问的其实是 target.foo。很显然,在副作用函数内通过原始对象访问它的某个属性是不会建立响应联系的

const target = {
   
 foo: 1,
 get bar() {
   
 // 现在这里的 this 为代理对象 p
 return this.foo
 }
 }
 //等价于

 effect(() => {
   
      // obj 是原始数据,不是代理对象,这样的访问不能够建立响应联系
      target.foo
      })

代理Object

响应系统应该拦截一切读取操作,以便当数据变化时能够正确地触发响应 前面我们使用 get 拦截函数去拦截对属性的读取操作。但在响应系统中,“读取”是一个很宽泛的概念,下面列出了所有读取操作

  1. 访问属性:obj.foo
  2. 判断对象或原型上是否存在给定的 key:key in obj
  3. 使用 for...in 循环遍历对象:for (const key in obj) {}

对于in操作符在has上就可以拦截

  has(target, key) {
   
   track(target, key)
   return Reflect.has(target, key)
   }

对于forin在ownKeys上拦截,因为ownKeys的参数里没有key,ownKey只判断是否存在属性,并不对应与某一个key,所以参数没有key也很正常.但是没有key,forin对应的副作用应该怎么存放呢?那就再声明一个新的symbol(ITERATE_KEY),把它当成key.

那么什么时候触发与ITERATE_KEY对应的副作用函数呢? 给对象添加新属性会影响forin循环的次数
所以只需要修改时把ITERATE_KEY对应的副作用函数也执行一遍

  let ITERATE_KEY = Symbol();
  ownKeys(target) {
   
   // 将副作用函数与 ITERATE_KEY 关联
   track(target, ITERATE_KEY)
   return Reflect.ownKeys(target)
   }
   effect(() => {
   
    for (const key in proxyData) {
   
    console.log('sss')
    }
    }
    )
function trigger(target, key) {
   
  let effects = bucket?.get(target)?.get(key);
  const iterateEffects = bucket?.get(target).get(ITERATE_KEY)//新增
  const effectsToRun = new Set([...(effects?effects:[]),...(iterateEffects?iterateEffects:[])]);//新增
  effectsToRun &&
    effectsToRun.forEach((effectFn) => {
   
      if (activeEffect !== effectFn) {
   
        effectFn.options.scheduler
          ? effectFn.options.scheduler(effectFn)
          : effectFn();
      }
    });
}

当只是修改属性不会对 for...in 循环产生影响,我们不需要触发副作用函数重新执行,否则会造成不必要的性能开销。所以要想解决上述问题,当设置属性操作发生时,就需要我们在set 拦截函数内能够区分操作的类型,到底是添加新属性还是设置已有属性,再把type参数传递给trigger函数。

  set(target, key, newVal) {
   
    // 如果属性不存在,则说明是在添加新属性,否则是设置已有属性
    const type = Object.prototype.hasOwnProperty.call(target,key) ? 'SET' : 'ADD'//新增
    target[key] = newVal;
    trigger(target, key,type);//新增
  },

  function trigger(target, key,type) {
   //新增type参数
  let effects = bucket?.get(target)?.get(key);
  const iterateEffects = bucket?.get(target).get(ITERATE_KEY)
  if (type === 'ADD') {
   //新增
     effectsToRun = new Set([...(effects?effects:[]),...(iterateEffects?iterateEffects:[])]);
  }
  else {
   //新增
    effectsToRun=new Set(effects)
  }
  effectsToRun &&
    effectsToRun.forEach((effectFn) => {
   
      if (activeEffect !== effectFn) {
   
        effectFn.options.scheduler
          ? effectFn.options.scheduler(effectFn)
          : effectFn();
      }
    });
}

删除键值也会影响forin,所以需要监听deleteProperty,那么当操作类型为 ADD 或 DELETE 时,需要触发与 ITERATE_KEY 相关联的副作用函数重新执行
(type === "ADD"||type === "DELETE")

  deleteProperty(target, key) {
   
    // 检查被操作的属性是否是对象自己的属性
    const hadKey = Object.prototype.hasOwnProperty.call(target, y);
    // 使用 Reflect.deleteProperty 完成属性的删除
    const res = Reflect.deleteProperty(target, key);

    if (res && hadKey) {
   
      // 只有当被删除的属性是对象自己的属性并且成功删除时,才触发更新,传递DELETE
      trigger(target, key, "DELETE");
    }

    return res;
  },

深度监听

下面这种情况就监听不到变化,需要借助递归,reactive函数返回封装之后的proxy

effect(() => {
   
    console.log(proxyData.obj.a);
});
proxyData.obj.a=222;
    get(target, key, receiver) {
   
      res=Reflect.get(target, key, receiver)
      track(target, key);
      if (typeof res === 'object' && res !== null) {
   
         return reactive(res)
         }
      return res
    },

当然有时不需要深度监听,可以在封装一层函数,而给里面的createReactive传递不同参数用于区分
对于只读属性,只需添加上第三个参数即可

function reactive(obj) {
   
  return createReactive(obj);
}
function shallowReactive(obj) {
   
  return createReactive(obj, true);
}
//在get中。isShallow是第二个参数代表是否深度监听,直接返回未封装的
if (isShallow) {
     return res  }
//对应只读,需要在触发set deleteProperty最开始判断第三个参数isReadonly,抛出错误信息并立即退出
//对应深只读,需在get中将返回包装成只读
if (typeof res === 'object' && res !== null) {
     
// 如果数据为只读,则调用 readonly 对值进行包装 
return isReadonly ? readonly(res) : reactive(res)  
}

代理数组

对数组的读取操作有

  1. 通过索引访问
  2. 访问数组的length
  3. forin forof 循环
  4. 数组的原型方法,如 concat/join/every/some/find/findIndex/includes 等,以及其他所有不改变原数组的原型方法。

设置操作

  1. 通过索引修改
  2. 修改length
  3. 数组的栈方法:push/pop/shift/unshift。
  4. 修改原数组的原型方法:splice/fill/sort

代理数组其实是代理对象很相似,大部分代码都不需要额外做处理

但是例如通过索引设置元素,可能会修改length,也就需要触发length相关的副作用执行

let arr = reactive([0])
effect(() => {
   
 console.log(arr.length) 
})
arr[1]=100

在set中判断是对数组的操作是add还是set,在trigger中判断如果是add并且是数组,要触发length相关的副作用

   set(target, key, newVal) {
   
      const type = Array.isArray(target)//新增
        ? // 如果代理目标是数组,则检测被设置的索引值是否小于数组长度,
          // 如果是,则视作 SET 操作,否则是 ADD 操作
          Number(key) < target.length
          ? "SET"
          : "ADD"
        : Object.prototype.hasOwnProperty.call(target, key)
        ? "SET"
        : "ADD";

      target[key] = newVal;
      trigger(target, key, type);
    },

    function trigger(target, key, type) {
   
  let effects = bucket?.get(target)?.get(key);
  const iterateEffects = bucket?.get(target)?.get(ITERATE_KEY);
  const lengthEffects = bucket?.get(target)?.get('length');
  if (type === 'ADD' && Array.isArray(target)) {
   //新增
    effectsToRun = new Set([
      ...(effects ? effects : []),
      ...(lengthEffects ? lengthEffects : []),
    ]);
  }
  else if (type === "ADD" || type === "DELETE") {
   
    effectsToRun = new Set([
      ...(effects ? effects : []),
      ...(iterateEffects ? iterateEffects : []),
    ]);
  } else {
   
    effectsToRun = new Set(effects);
  }
  effectsToRun &&
    effectsToRun.forEach((effectFn) => {
   
      if (activeEffect !== effectFn) {
   
        effectFn.options.scheduler
          ? effectFn.options.scheduler(effectFn)
          : effectFn();
      }
    });
}

然而当修改 length 属性值时也会隐式地影响数组元素,会对只有那些索引值大于或等于新的 length 属性值的元素产生影响,需要触发响应。所以给trigger再传入一个参数也就是修改之后的长度length,再将相关的副作用添加再执行


const arr = reactive(['foo'])
 effect(() => {
   
 // 访问数组的第 0 个元素
 console.log(arr[0]) // foo
 })
 // 将数组的长度修改为 0,导致第 0 个元素被删除,因此应该触发响应
 arr.length = 0
   set(target, key, newVal,receiver) {
   
      const type = Array.isArray(target) 
        ? 
          Number(key) < target.length
          ? "SET"
          : "ADD"
        : Object.prototype.hasOwnProperty.call(target, key)
        ? "SET"
        : "ADD";
      trigger(target, key, type, newVal);//多传一个值
      return Reflect.set(target, key, newVal, receiver);
    },


function trigger(target, key, type, newVal) {
   
  let effects = bucket?.get(target)?.get(key);
  const iterateEffects = bucket?.get(target)?.get(ITERATE_KEY);
  const lengthEffects = bucket?.get(target)?.get("length");
 if (type === "ADD" && Array.isArray(target)) {
   
    effectsToRun = new Set([
      ...(effects ? effects : []),
      ...(lengthEffects ? lengthEffects : []),
    ]);
  } else if (type === "ADD" || type === "DELETE") {
   
    effectsToRun = new Set([
      ...(effects ? effects : []),
      ...(iterateEffects ? iterateEffects : []),
    ]);
  } else {
   
    effectsToRun = new Set(effects);
  }
  if (Array.isArray(target) && key === "length") {
   //新增
    let depsMap = bucket?.get(target);
    // 对于索引大于或等于新的 length 值的元素,
    // 需要把所有相关联的副作用函数取出并添加到 effectsToRun 中待执行
    depsMap.forEach((effects, key) => {
   
      if (key >= newVal) {
   
        effects.forEach((effectFn) => {
   
          if (effectFn !== activeEffect) {
   
            effectsToRun.add(effectFn);
          }
        });
      }
    });
  } 
  effectsToRun &&
    effectsToRun.forEach((effectFn) => {
   
      if (activeEffect !== effectFn) {
   
        effectFn.options.scheduler
          ? effectFn.options.scheduler(effectFn)
          : effectFn();
      }
    });
}

为了追踪对普通对象的 for...in 操作,人为创造了 ITERATE_KEY 作为追踪的 key,但是对应遍历数组,只需要使用 length 作为 key 去建立响应联系,因为只有修改length才会影响forin

对于for of与for in的代码相同,但是它还会读取数组的 Symbol.iterator 属性。该属性是一个 symbol值,为了避免发生意外的错误,以及性能上的考虑,我们不应该在副作用函数与Symbol.iterator 这类 symbol 值之间建立响应联系,因此需要修改 get 拦截函数

 ownKeys(target) {
   
      track(target, Array.isArray(target) ? 'length' :ITERATE_KEY)//修改
      return Reflect.ownKeys(target);
    },
    //get
    if (typeof key !== 'symbol') {
     track(target, key)  }

重写数组方法,为什么要重写

首先要知道当调用数组的 push 方法向数组中添加元素时,既会读取数组的 length 属性值,也会设置数组的 length 属性值。这会导致两个独立的副作用函数互相影响。以下面的代码为例:

// 第一个副作用函数
effect(() => {
   
  arr.push(1);
});

// 第二个副作用函数
effect(() => {
   
  arr.push(1);
});

执行过程

  1. 第一个副作用函数执行。在该函数内,调用 arr.push 方法向数 组中添加了一个元素。我们知道,调用数组的 push 方法会间接读取数组的 length 属性。所以,当第一个副作用函数执行完毕后,会与 length 属性建立响应联系。
  2. 接着,第二个副作用函数执行。同样,它也会与 length 属性建立响应联系。但不要忘记,调用 arr.push 方法不仅会间接读取数组的 length 属性,还会间接设置 length 属性的值。
  3. 第二个函数内的 arr.push 方法的调用设置了数组的 length 属性值。于是,响应系统尝试把与 length 属性相关联的副作用函 数全部取出并执行,其中就包括第一个副作用函数。问题就出在这里,可以发现,第二个副作用函数还未执行完毕,就要再次执 行第一个副作用函数了。
  4. 第一个副作用函数再次执行。同样,这会间接设置数组的 length 属性。于是,响应系统又要尝试把所有与 length 属性相关联的副作用函数取出并执行,其中就包含第二个副作用函数。
  5. 如此循环往复,最终导致调用栈溢出。

问题的原因是 push 方法的调用会间接读取 length 属性。所以,只要我们“屏蔽”对 length 属性的读取,从而避免在它与副作用函数之间建立响应联系

并且因为数组的 push 方法在语义上是修改操作,而非读取操作,所以避免建立响应联系并不会产生其他副作用

let shouldTrack = true;
// 重写数组的 push、pop、shift、unshift 以及 splice 方法
["push", "pop", "shift", "unshift", "splice"].forEach((method) => {
   
  const originMethod = Array.prototype[method];
  arrayInstrumentations[method] = function (...args) {
   
    shouldTrack = false;
    let res = originMethod.apply(this, args);
    shouldTrack = true;
    return res;
  };
});

function track(target, key) {
   
  // 当禁止追踪时,直接返回
  if (!activeEffect || !shouldTrack) return;
  // 省略部分代码
}

代理set map

使用 Proxy 代理集合类型的数据不同于代理普通对象,因为集合类型数据的操作与普通对象存在很大的不同,这里暂不往下深入

Set 类型的原型属性和方法如下。

size:返回集合中元素的数量。
add(value):向集合中添加给定的值。
clear():清空集合。
delete(value):从集合中删除给定的值。
has(value):判断集合中是否存在给定的值。
keys():返回一个迭代器对象。可用于 for...of 循环,迭代器对象产生的值为集合中的元素值。
values():对于 Set 集合类型来说,keys() 与 values() 等价。
entries():返回一个迭代器对象。迭代过程中为集合中的每一个元素产生一个数组值 [key, value]。
forEach(callback[, thisArg]):forEach 函数会遍历集合中的所有元素,并对每一个元素调用 callback 函数。
forEach 函数接收可选的第二个参数 thisArg,用于指定callback 函数执行时的 this 值。

Map 类型的原型属性和方法如下。

size:返回 Map 数据中的键值对数量。
clear():清空 Map。
delete(key):删除指定 key 的键值对。
has(key):判断 Map 中是否存在指定 key 的键值对。
get(key):读取指定 key 对应的值。
set(key, value):为 Map 设置新的键值对。
keys():返回一个迭代器对象。迭代过程中会产生键值对的 key值。
values():返回一个迭代器对象。迭代过程中会产生键值对的value 值。
entries():返回一个迭代器对象。迭代过程中会产生由 [key,value] 组成的数组值。
forEach(callback[, thisArg]):forEach 函数会遍历Map 数据的所有键值对,并对每一个键值对调用 callback 函数。forEach 函数接收可选的第二个参数 thisArg,用于指定callback 函数执行时的 this 值。

相关文章
|
3天前
|
缓存 JavaScript UED
Vue3中v-model在处理自定义组件双向数据绑定时有哪些注意事项?
在使用`v-model`处理自定义组件双向数据绑定时,要仔细考虑各种因素,确保数据的准确传递和更新,同时提供良好的用户体验和代码可维护性。通过合理的设计和注意事项的遵循,能够更好地发挥`v-model`的优势,实现高效的双向数据绑定效果。
106 64
|
3天前
|
JavaScript 前端开发 API
Vue 3 中 v-model 与 Vue 2 中 v-model 的区别是什么?
总的来说,Vue 3 中的 `v-model` 在灵活性、与组合式 API 的结合、对自定义组件的支持等方面都有了明显的提升和改进,使其更适应现代前端开发的需求和趋势。但需要注意的是,在迁移过程中可能需要对一些代码进行调整和适配。
|
25天前
|
JavaScript 前端开发 开发者
Vue 3中的Proxy
【10月更文挑战第23天】Vue 3中的`Proxy`为响应式系统带来了更强大、更灵活的功能,解决了Vue 2中响应式系统的一些局限性,同时在性能方面也有一定的提升,为开发者提供了更好的开发体验和性能保障。
51 7
|
25天前
|
JavaScript 数据管理 Java
在 Vue 3 中使用 Proxy 实现数据双向绑定的性能如何?
【10月更文挑战第23天】Vue 3中使用Proxy实现数据双向绑定在多个方面都带来了性能的提升,从更高效的响应式追踪、更好的初始化性能、对数组操作的优化到更优的内存管理等,使得Vue 3在处理复杂的应用场景和大量数据时能够更加高效和稳定地运行。
40 1
|
25天前
|
JavaScript 开发者
在 Vue 3 中使用 Proxy 实现数据的双向绑定
【10月更文挑战第23天】Vue 3利用 `Proxy` 实现了数据的双向绑定,无论是使用内置的指令如 `v-model`,还是通过自定义事件或自定义指令,都能够方便地实现数据与视图之间的双向交互,满足不同场景下的开发需求。
46 1
|
10天前
|
缓存 JavaScript 前端开发
vue学习第四章
欢迎来到我的博客!我是瑞雨溪,一名热爱JavaScript与Vue的大一学生。本文介绍了Vue中计算属性的基本与复杂使用、setter/getter、与methods的对比及与侦听器的总结。如果你觉得有用,请关注我,将持续更新更多优质内容!🎉🎉🎉
vue学习第四章
|
10天前
|
JavaScript 前端开发
vue学习第九章(v-model)
欢迎来到我的博客,我是瑞雨溪,一名热爱JavaScript与Vue的大一学生,自学前端2年半,正向全栈进发。此篇介绍v-model在不同表单元素中的应用及修饰符的使用,希望能对你有所帮助。关注我,持续更新中!🎉🎉🎉
vue学习第九章(v-model)
|
10天前
|
JavaScript 前端开发 开发者
vue学习第十章(组件开发)
欢迎来到瑞雨溪的博客,一名热爱JavaScript与Vue的大一学生。本文深入讲解Vue组件的基本使用、全局与局部组件、父子组件通信及数据传递等内容,适合前端开发者学习参考。持续更新中,期待您的关注!🎉🎉🎉
vue学习第十章(组件开发)
|
16天前
|
JavaScript 前端开发
如何在 Vue 项目中配置 Tree Shaking?
通过以上针对 Webpack 或 Rollup 的配置方法,就可以在 Vue 项目中有效地启用 Tree Shaking,从而优化项目的打包体积,提高项目的性能和加载速度。在实际配置过程中,需要根据项目的具体情况和需求,对配置进行适当的调整和优化。
|
16天前
|
存储 缓存 JavaScript
在 Vue 中使用 computed 和 watch 时,性能问题探讨
本文探讨了在 Vue.js 中使用 computed 计算属性和 watch 监听器时可能遇到的性能问题,并提供了优化建议,帮助开发者提高应用性能。