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 值。

相关文章
|
2月前
|
缓存 JavaScript UED
Vue3中v-model在处理自定义组件双向数据绑定时有哪些注意事项?
在使用`v-model`处理自定义组件双向数据绑定时,要仔细考虑各种因素,确保数据的准确传递和更新,同时提供良好的用户体验和代码可维护性。通过合理的设计和注意事项的遵循,能够更好地发挥`v-model`的优势,实现高效的双向数据绑定效果。
153 64
|
2月前
|
JavaScript 前端开发 API
Vue 3 中 v-model 与 Vue 2 中 v-model 的区别是什么?
总的来说,Vue 3 中的 `v-model` 在灵活性、与组合式 API 的结合、对自定义组件的支持等方面都有了明显的提升和改进,使其更适应现代前端开发的需求和趋势。但需要注意的是,在迁移过程中可能需要对一些代码进行调整和适配。
119 60
|
14天前
|
JavaScript API 数据处理
vue3使用pinia中的actions,需要调用接口的话
通过上述步骤,您可以在Vue 3中使用Pinia和actions来管理状态并调用API接口。Pinia的简洁设计使得状态管理和异步操作更加直观和易于维护。无论是安装配置、创建Store还是在组件中使用Store,都能轻松实现高效的状态管理和数据处理。
49 3
|
2月前
|
前端开发 JavaScript 测试技术
Vue3中v-model在处理自定义组件双向数据绑定时,如何避免循环引用?
Web 组件化是一种有效的开发方法,可以提高项目的质量、效率和可维护性。在实际项目中,要结合项目的具体情况,合理应用 Web 组件化的理念和技术,实现项目的成功实施和交付。通过不断地探索和实践,将 Web 组件化的优势充分发挥出来,为前端开发领域的发展做出贡献。
42 8
|
2月前
|
存储 JavaScript 数据管理
除了provide/inject,Vue3中还有哪些方式可以避免v-model的循环引用?
需要注意的是,在实际开发中,应根据具体的项目需求和组件结构来选择合适的方式来避免`v-model`的循环引用。同时,要综合考虑代码的可读性、可维护性和性能等因素,以确保系统的稳定和高效运行。
37 1
|
2月前
|
JavaScript
Vue3中使用provide/inject来避免v-model的循环引用
`provide`和`inject`是 Vue 3 中非常有用的特性,在处理一些复杂的组件间通信问题时,可以提供一种灵活的解决方案。通过合理使用它们,可以帮助我们更好地避免`v-model`的循环引用问题,提高代码的质量和可维护性。
46 1
|
2月前
|
JavaScript
在 Vue 3 中,如何使用 v-model 来处理自定义组件的双向数据绑定?
需要注意的是,在实际开发中,根据具体的业务需求和组件设计,可能需要对上述步骤进行适当的调整和优化,以确保双向数据绑定的正确性和稳定性。同时,深入理解 Vue 3 的响应式机制和组件通信原理,将有助于更好地运用 `v-model` 实现自定义组件的双向数据绑定。
|
8天前
|
JavaScript
vue使用iconfont图标
vue使用iconfont图标
57 1
|
19天前
|
JavaScript 关系型数据库 MySQL
基于VUE的校园二手交易平台系统设计与实现毕业设计论文模板
基于Vue的校园二手交易平台是一款专为校园用户设计的在线交易系统,提供简洁高效、安全可靠的二手商品买卖环境。平台利用Vue框架的响应式数据绑定和组件化特性,实现用户友好的界面,方便商品浏览、发布与管理。该系统采用Node.js、MySQL及B/S架构,确保稳定性和多功能模块设计,涵盖管理员和用户功能模块,促进物品循环使用,降低开销,提升环保意识,助力绿色校园文化建设。
|
2月前
|
JavaScript 前端开发 开发者
vue学习第一章
欢迎来到我的博客!我是瑞雨溪,一名热爱前端的大一学生,专注于JavaScript与Vue,正向全栈进发。博客分享Vue学习心得、命令式与声明式编程对比、列表展示及计数器案例等。关注我,持续更新中!🎉🎉🎉
50 1
vue学习第一章