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

相关文章
|
4天前
|
开发工具 iOS开发 MacOS
基于Vite7.1+Vue3+Pinia3+ArcoDesign网页版webos后台模板
最新版研发vite7+vue3.5+pinia3+arco-design仿macos/windows风格网页版OS系统Vite-Vue3-WebOS。
70 10
|
4月前
|
缓存 JavaScript PHP
斩获开发者口碑!SnowAdmin:基于 Vue3 的高颜值后台管理系统,3 步极速上手!
SnowAdmin 是一款基于 Vue3/TypeScript/Arco Design 的开源后台管理框架,以“清新优雅、开箱即用”为核心设计理念。提供角色权限精细化管理、多主题与暗黑模式切换、动态路由与页面缓存等功能,支持代码规范自动化校验及丰富组件库。通过模块化设计与前沿技术栈(Vite5/Pinia),显著提升开发效率,适合团队协作与长期维护。项目地址:[GitHub](https://github.com/WANG-Fan0912/SnowAdmin)。
730 5
|
1月前
|
缓存 前端开发 大数据
虚拟列表在Vue3中的具体应用场景有哪些?
虚拟列表在 Vue3 中通过仅渲染可视区域内容,显著提升大数据列表性能,适用于 ERP 表格、聊天界面、社交媒体、阅读器、日历及树形结构等场景,结合 `vue-virtual-scroller` 等工具可实现高效滚动与交互体验。
242 1
|
1月前
|
缓存 JavaScript UED
除了循环引用,Vue3还有哪些常见的性能优化技巧?
除了循环引用,Vue3还有哪些常见的性能优化技巧?
142 0
|
2月前
|
JavaScript
vue3循环引用自已实现
当渲染大量数据列表时,使用虚拟列表只渲染可视区域的内容,显著减少 DOM 节点数量。
92 0
|
4月前
|
JavaScript API 容器
Vue 3 中的 nextTick 使用详解与实战案例
Vue 3 中的 nextTick 使用详解与实战案例 在 Vue 3 的日常开发中,我们经常需要在数据变化后等待 DOM 更新完成再执行某些操作。此时,nextTick 就成了一个不可或缺的工具。本文将介绍 nextTick 的基本用法,并通过三个实战案例,展示它在表单验证、弹窗动画、自动聚焦等场景中的实际应用。
404 17
|
4月前
|
JavaScript 前端开发 API
Vue 2 与 Vue 3 的区别:深度对比与迁移指南
Vue.js 是一个用于构建用户界面的渐进式 JavaScript 框架,在过去的几年里,Vue 2 一直是前端开发中的重要工具。而 Vue 3 作为其升级版本,带来了许多显著的改进和新特性。在本文中,我们将深入比较 Vue 2 和 Vue 3 的主要区别,帮助开发者更好地理解这两个版本之间的变化,并提供迁移建议。 1. Vue 3 的新特性概述 Vue 3 引入了许多新特性,使得开发体验更加流畅、灵活。以下是 Vue 3 的一些关键改进: 1.1 Composition API Composition API 是 Vue 3 的核心新特性之一。它改变了 Vue 组件的代码结构,使得逻辑组
1494 0
|
7天前
|
JavaScript
Vue中如何实现兄弟组件之间的通信
在Vue中,兄弟组件可通过父组件中转、事件总线、Vuex/Pinia或provide/inject实现通信。小型项目推荐父组件中转或事件总线,大型项目建议使用Pinia等状态管理工具,确保数据流清晰可控,避免内存泄漏。
90 2
|
3月前
|
人工智能 JavaScript 算法
Vue 中 key 属性的深入解析:改变 key 导致组件销毁与重建
Vue 中 key 属性的深入解析:改变 key 导致组件销毁与重建
527 0
|
3月前
|
JavaScript UED
用组件懒加载优化Vue应用性能
用组件懒加载优化Vue应用性能