不会Reflect,怎么玩转Proxy --Proxy篇

前言

Proxy作为vue3的核心api之一,它的作用相当于vue3的顶梁柱,响应式及双向绑定都利用Proxy的特性来完成的。现在熟悉vue3的谁不知道大名鼎鼎的Proxy。但Proxy的方法和使用都知道吗?

上一篇说了Proxy的好兄弟Reflect,它们俩搭配使用可以让代码看起来更有灵魂,也可以在写代码的时候减少心理负担,废话不多说,开始介绍一下Proxy的方法即使用

上一篇:不会Reflect,怎么玩转Proxy --Reflect篇 - 掘金 (juejin.cn)

正文

与Reflect的数量相同并且方法名都一样,都有13个方法
get
set
has
ownKeys
apply
deleteProperty
definProperty
getOwnPropertyDescriptor
getPrototypeOf
setPrototypeOf
preventExtensions
isExtensible
construct

get 用于拦截对象属性读取操作

get方法用于在读取被拦截对象的属性,当读取对象属性值的时候就会触发此方法,传递三个参数对象本身,读取的key,以及this指向,需要返回一个属性值。看起来是不是很熟悉,Reflect.get需要接收三个参数,并返回读取的值

js 复制代码
const obj = {
  name: "iceCode",
  age: 24,
};
//首先对对象拦截,在get方法中返回Number 10
const objP = new Proxy(obj, {
  get(target, key, receiver) {
    return 10;
  },
});
//这个时候不管访问什么都是数字10
console.log(objP.name);//10
console.log(objP.age);//10

正常拦截肯定只是想进行一些操作,然后返回一个值

js 复制代码
const objP = new Proxy(obj, {
  get(target, key, receiver) {
    let x = target[key];
    x = x + 1;
    return x;
  },
});
//这样就返回一个进行执行之后的值
console.log(objP.name);//iceCode1
console.log(objP.age);//25

当然访问的时候肯定希望返回原来的值,不希望改变原有的值

js 复制代码
const objP = new Proxy(obj, {
  get(target, key, receiver) {
    //....
    return target[key];
  },
});
//这样就可以确保我们访问的时候,都可以读取原有的值
console.log(objP.name);//iceCode
console.log(objP.age);//24

但我们不敢保证所返回出的所有的值它的this指向都是对的,这个时候就可以使用Reflect的对应方法了

js 复制代码
const objP = new Proxy(obj, {
  get(target, key, receiver) {
    //....
    //Reflect.get方法接收这个对象,key值,this指向,确保它的this永远都指向目标对象,返回key的属性值
    return Reflect.get(target, key, receiver);
    //上面看着有些繁琐,也可以更简洁一些
    //首先我们已知都知道函数内都有一个arguments对象
    //其次我们还知道Proxy方法传递的值和Reflect相同方法所接受的值是一致的
    //那么等式成立
    return Reflect.get(...arguments)//这两个使用方式存在其一即可
    
  },
});
console.log(objP.name);
console.log(objP.age);

搭配Reflect不仅可以优雅的查找到我们效果要的数据,也可以确保我们的this永远不会改变 ,指向被拦截的这个目标对象,后续方法将全部搭配Reflect的方法来写例子

set 用于拦截对象属性设置(修改)操作

set方法在对象属性被修改时触发,传递四个参数,对象本身,key值,要修改的值,this指向,需要返回一个布尔值,来表示是否修改成功

js 复制代码
const obj = {
  name: "iceCode",
  age: 24,
};
const objP = new Proxy(obj, {
  set(target, key, value, receiver) {
    //....
    console.log(`此时被修改的是${key},修改的值是${value},类型为${typeof value}`);
    return Reflect.set(...arguments);//属性能被修改,此方法一定会返回true,否则false
  },
});
//当修改时,就会触发set方法,并打印set方法里的日志
objP.age = 18;//此时被修改的是age,修改的值是18,类型为number
console.log(objP);//{ name: 'iceCode', age: 18 }

注意 :set方法传递的第四个参数,指向的不一定是当前被拦截的对象本身。假设有一段代码执行 obj.name = "jen"obj 不是一个 proxy,且自身不含 name 属性,但是它的原型链上有一个 proxy,那么,那个 proxy 的 set() 处理器会被调用,而此时,obj 会作为 receiver 参数传进来。当然使用了Reflect.set方法之后,也可以忽略这个关注点.

has 用于in操作符的拦截

has主要就是对in操作符的拦截,也就是在对象中能否查找到这个属性,传递两个参数,对象本身和key,返回一个布尔值,true为可以被in操作符查找到,false则找不到

js 复制代码
const obj = {
  _name: "内部名字",
  name: "iceCode",
  age: 24,
};
const objP = new Proxy(obj, {
  has(target, key) {
    //判定条件,带有'_'的都将不会被查到
    if (key.indexOf("_") > -1) {
      return false;
    }
    return Reflect.has(...arguments);
  },
});
console.log("_name" in objP);//false
console.log("name" in objP);//true

//但是这个方法在使用for in 遍历的时候是不会触发的,也就无法进行拦截
for (const key in objP) {
  console.log(key);
}
//_name
//name
//age

ownKeys 主要对Reflect.ownKeys方法的拦截

此方法一般很少用,本身Reflect.ownKeys方法就是为方便了查找不可便利的对象属性,传入一个参数,对象本身,需要返回一个可枚举的对象,必须是一个数组

js 复制代码
const sTar = Symbol("tar");
const sTal = Symbol("tal");

const obj = {
  [sTar]: "tar",
  [sTal]: "tal",
  name: "iceCode",
  age: 24,
};
const objP = new Proxy(obj, {
  ownKeys(target) {
    //返回可枚举的对象,数组类型
    return Reflect.ownKeys(target);
  },
});
//但是在这里结果是不会改变的,Symbol类型还是无法遍历出来,Reflect.ownKeys是无法迭代的
//如果使用for of要使用Object.keys转换一下
for (const key of Object.keys(objP)) {
  console.log(key);
}
//name
//age

deleteProperty 用于delete对象属性的拦截

在我们使用delete对对象属性删除的时候,会触发delelteProperty方法,传递两个参数,对象本身,要删除的key,需要返回一个布尔值,标识属性是否被成功删除

js 复制代码
const obj = {
  _name: "内部名称",
  name: "iceCode",
  age: 24,
};
const objP = new Proxy(obj, {
  deleteProperty(target, key) {
  //当key存在有'_'的时候,是不能被删除的,否者会提示错误
    if (key.includes("_")) {
      console.error(new Error("不能删除内部属性"));
      return false;
    }
    return Reflect.deleteProperty(...arguments);
  },
});
delete objP.age;
console.log(objP);//{ _name: '内部名称', name: 'iceCode' }
delete objP._name;//在这里将不产生任何作用

definProperty 用于Object.definProperty方法的拦截

当使用Object.definPropertyProxy对象进行劫持的时候,就会触发此方法,传递三个参数,对象本身,key,劫持方法,返回一个布尔值,用于标识劫持方法是否操作成功。(Object.definProperty方法应该都比较熟悉了,主要对对象属性是否可写,是否可遍历,是否可读进行一些操作)

js 复制代码
const obj = {
  name: "iceCode",
  age: 24,
};
const objP = new Proxy(obj, {
  defineProperty(target, key, descriptor) {
      //这里就可以看出,对哪个属性进行劫持,并使用了哪些劫持操作
    console.log(`对${key}的劫持方法:${JSON.stringify(descriptor)}`);
    //对name的劫持方法:{"value":"iceCode--new","enumerable":false,"configurable":true}
    return Reflect.defineProperty(...arguments);
  },
});
//当我们对拦截对象进行劫持对应的属性时,就会触发defineProperty方法
Object.defineProperty(objP, "name", {
  enumerable: false,
  configurable: true,
  value: "iceCode--new",
});
//

getOwnPropertyDescriptor 用于Object.getOwnPropertyDescriptor的拦截方法

当使用Object.getOwnPropertyDescriptor方法时,会返回描述该属性的的配置,Proxy上的getOwnPropertyDescriptor方法也可以说是它的钩子,当Object.getOwnPropertyDescriptor访问一个对象属性的时候,就会触发此方法,传递两个参数,对象本身,key,需要一个返回值,一个对象或者undefined

js 复制代码
const obj = {
  name: "iceCode",
  age: 24,
};
const objP = new Proxy(obj, {
  getOwnPropertyDescriptor(target, key) {
  //当使用了Object.getOwnPropertyDescriptor就出触发
    console.log(`对${key}属性使用了Object.getOwnPropertyDescriptor方法`);
    //对name属性使用了Object.getOwnPropertyDescriptor方法
    return Reflect.getOwnPropertyDescriptor(...arguments);
  },
});
//
const d = Object.getOwnPropertyDescriptor(objP, "name");
console.log(d);
//{
//  value: 'iceCode',
//  writable: true,
//  enumerable: true,
//  configurable: true
//}

getPrototypeOf 当读取对象原型的时候,就会触发此拦截方法

当访问一个对象的原型的时候,就会触发此方法,传递一个参数,对象本身,需要一个返回值,一个对象或null

js 复制代码
const obj = {
  name: "iceCode",
  age: 24,
};
const objP = new Proxy(obj, {
  getPrototypeOf(target) {
  //每次触发都会打印日志
    console.log(target, "eeeeee", this);
    return Reflect.getPrototypeOf(...arguments);
  },
});
//以下是五种触发getPrototypeOf方法的方式
console.log(
  Object.getPrototypeOf(objP) === Object.prototype, // true
  Reflect.getPrototypeOf(objP) === Object.prototype, // true
  objP.__proto__ === Object.prototype, // true
  Object.prototype.isPrototypeOf(objP), // true
  objP instanceof Object // true
);

setPrototypeOf 主要拦截Object.setPrototypeOf

当使用Object.setPrototypeOf对对象的原型进行修改的时候,就会触发这个方法,传入两个参数,对象本身,对象的新原型或者null,需要返回一个布尔值,标识原型是否被修改成功

js 复制代码
const obj = {
  name: "iceCode",
  age: 24,
};
const objP = new Proxy(obj, {
 //使用Object.setPrototypeOf修改原型的时候,会触发此方法
  setPrototypeOf(target, prototype) {
    console.log(target, prototype);//{ name: 'iceCode', age: 24 } { age: 100 }
    return Reflect.setPrototypeOf(...arguments);
  },
});
//这里修改objP的原型
Object.setPrototypeOf(objP, { age: 100 });

preventExtensions 用于对Object.preventExtensions的拦截

当使用Object.preventExtensions对一个对象禁止扩展时,就会触发此方法,传入一个参数,对象本身,需要返回一个布尔值,标识是否禁止扩展成功

js 复制代码
const obj = {
  name: "iceCode",
  age: 24,
};
const obj2 = {
  name: "iCode",
  age: 24,
};
const retProxy = (v) =>
  new Proxy(v, {
    preventExtensions(target) {
       //当被拦截的对属性name为iceCode的时候返回false
      if (target.name === "iceCode") {
        return false;
      }
      return Reflect.preventExtensions(...arguments);
    },
  });
const objP = retProxy(obj);
const objP1 = retProxy(obj2);
//当拦截返回false 在使用Object.preventExtensions进行禁止扩展会报错:'preventExtensions' on proxy: trap returned falsish
Object.preventExtensions(objP);
//这里时可以正常禁止扩展的
Object.preventExtensions(objP1);
objP.tel = 123;//这个地方根本就走不到
objP1.tel = 123;//因为被禁止扩展了,这里也无法被添加新的属性

isExtensible 用于对Object.isExtensible的拦截

当访问一个对象是否可扩展,使用Object.isExtensible方法时,就会触发此方法,传递一个参数,对象本身,需要返回一个布尔值或者可以转成布尔值的值,例如1,会默认转成true。 Object.isExtensible(proxy) 必须同 Object.isExtensible(target) 返回相同值。

js 复制代码
const obj = {
  name: "iceCode",
  age: 24,
};
const obj2 = {
  name: "iCode",
  age: 24,
};
const retProxy = (v) =>
  new Proxy(v, {
     //拦截对象被Object.isExtensible使用时就会触发此方法,但必须两个的返回值相同
    isExtensible(target) {
      if (target.name === "iceCode") {
        return false;
      }
      return Reflect.isExtensible(...arguments);
    },
  });
const objP = retProxy(obj);
const objP1 = retProxy(obj2);
//这里的返回值不同就会报错:'isExtensible' on proxy: trap result does not reflect extensibility of proxy target (which is 'true')
console.log(Object.isExtensible(objP));
console.log(Object.isExtensible(objP1));//true

construct 用于拦截new操作符

当使用Proxy拦截一个构造函数的时候,返回一个目标对象,使用new操作符时,会触发此方法,传入三个参数,构造函数本身,构造函数参数列表,新的构造函数(用于改变this),需要返回一个对象,必须是对象类型

js 复制代码
const Obj = function (v, t) {
  console.log(v, t);//iceCode 24
  this.name = v;
  this.age = t;
};
//代理一个构造函数,返回的目标函数依旧是个构造函数
const objP = new Proxy(Obj, {
  construct(target, args, newTarget) {
  //正常情况下,traget===newTarget
    console.log("construct", target, args, newTarget);//construct [Function: Obj] [ 'iceCode', 24 ] [Function: Obj]
    return Reflect.construct(...arguments);
  },
});
//当使用new操作符时,就会触发Proxy的construct方法
const p = new objP("iceCode", 24);
console.log(p);//Obj { name: 'iceCode', age: 24 }

总结

Proxy的13种方法各有各的用处,但平常我们用不到,即使是写Proxy的时候,大多情况下也只能用到getset等方法,像对Object方法的拦截,需求少之又少。另外也发现了,Proxy的每一个方法都需要返回值,返回值也都不是固定的某些值,当使用了Reflect对应得方法之后,完全不就不用再考虑Proxy这些方法需要返回哪些值,只要返回的是Reflect对应的方法,就永远不会出错。

在HTML文件中实现双向绑定

讲了Proxy的方法,但是要如何使用,这里就极简的在HTML文件中实现一个双向绑定例子

首先要定义元素并获取元素,再使用Proxy的set方法,修改对象值得时候,会进行一些dom操作

js 复制代码
 <body>
 //创建两个元素
    <input type="text" placeholder="描述" class="des" />
    <div class="box"></div>
  </body>
  <script>
  //获取元素
    const des = document.querySelector(".des");
    const div = document.querySelector("div");
    //给input添加input事件
    des.addEventListener("input", (e) => {
    //每次触发input事件都会更改formProxy对象里面属性的值
      formProxy.des = e.target.value;
    });
    //为了简便操作,这里直接代理一个空对象
    const formProxy = new Proxy({}, {
      get(traget, key, receiver) {
        return Reflect.get(...arguments);
      },
      //在更改代理对象里面属性值的时候,就会触发set事件
      set(target, key, value, receiver) {
      //在set事件里面拿到更改的值value 赋值到要显示的地方
        div.innerText = value;//给div
        des.value = value;//给input
        return Reflect.set(...arguments);
      },
    });
   </script>

这样究极简单的双向绑定就完成了,是不是超级简单,会React的应该比较熟悉input那里的赋值操作

js 复制代码
//这样看着应该很熟悉
 <input type="text" value={'变量'} onChange={(e)=>修改变量的方法(e.target.value)}>

看的出已经成功了,input里的数据成功映射到div里面

这应该算是最简单的双向绑定吧⊙﹏⊙∥

利用Proxy实现toDoList

可能使用input映射到div里太过于简单,好像这种方式在input事件里面也可以实现,也并不像双向绑定也没啥优势。那就再加一个示例,在原来基础上写一个toDoList

js 复制代码
 <body>
 
    <input type="text" placeholder="描述" class="des" />
    //创建添加按钮
    <button>添加</button>
    <div class="box"></div>
    //列表元素
    <ul class="ul"></ul>
  </body>
  <script>
  //获取dom
    const des = document.querySelector(".des");
    const button = document.querySelector("button");
    const div = document.querySelector("div");
    const ul = document.querySelector(".ul");
    des.addEventListener("input", (e) => {
      formProxy.des = e.target.value;
    });
    //给添加按钮添加点击事件
    button.addEventListener("click", () => {
    //代理对象,是一个数组,当我们点击添加按钮的时候,将formProxy代理对象的值添加到数组中
      addLi.push(formProxy.des);
      //然后将formProxy属性的值清空,这里只要清空了数据,input元素和div元素中的内容都会被清空
      //这就是利用Proxy双向绑定的好处,只需要处理数据就行了
      formProxy.des = "";
      console.log(addLi);
    });
    //代理对象
    const formProxy = new Proxy(
      {},
      {
        get(traget, key, receiver) {
          return Reflect.get(...arguments);
        },
        set(target, key, value, receiver) {
          console.log(value);
          div.innerText = value;
          des.value = value;
          return Reflect.set(...arguments);
        },
      }
    );
    //代理数组
    const addLi = new Proxy([], {
      get() {
        return Reflect.get(...arguments);
      },
      set(traget, key, value, receiver) {
      //当push元素的时候,set方法会调用两次,我们只需要对我们有用的数据即可
      //这里过滤以下,代理数组的时候,key是string类型的索引
        if (key !== "length") {
        //创建元素,并添加到ul中
          const li = document.createElement("li");
          li.innerText = value;
          ul.appendChild(li);
        }
        return Reflect.set(...arguments);
      },
      } );
     </script> 

这样 简单的双向绑定ToDoList也就完成了

当然只是添加也不行,还需要删除操作,这个时候就需要在代理数组的set方法中添加一些简单的dom操作就可以了,但是需要改进一下。

js 复制代码
//元素还是与之前一样
  <body>
    <input type="text" placeholder="描述" class="des" />
    <button>添加</button>
    <div class="box"></div>
    <ul class="ul"></ul>
  </body>
  <script>
    const des = document.querySelector(".des");
    const button = document.querySelector("button");
    const div = document.querySelector("div");
    const ul = document.querySelector(".ul");
    des.addEventListener("input", (e) => {
      formProxy.des = e.target.value;
    });
    //添加操作
    button.addEventListener("click", () => {
      addLi.push(formProxy.des);
      formProxy.des = "";
    });
    //对象代理
    const formProxy = new Proxy(
      {},
      {
        get(traget, key, receiver) {
          return Reflect.get(...arguments);
        },
        set(target, key, value, receiver) {
          div.innerText = value;
          des.value = value;
          return Reflect.set(...arguments);
        },
      }
    );
    //数组代理
    const addLi = new Proxy([], {
      get() {
        return Reflect.get(...arguments);
      },
      //主要改变时在修改数据时的dom操作
      set(traget, key, value, receiver) {
      //这里修改了对dom的操作的方法,利用了最后一次返回数据
      因为只有最后一次的key不是索引,所以利用这一点,只要最后一次
        if (isNaN(parseInt(key))) {
        //每次执行将ul里面的内容清空,也就是li元素
          ul.innerHTML = "";
          //一般情况下traget和receiver是相同的数组,看个人喜欢来遍历循环添加li元素,因为空元素不会添加
          traget.forEach((item) => {
          //正常的dom添加
            const li = document.createElement("li");
            li.innerText = item;
            //再给li里添加删除按钮
            const but = document.createElement("button");
            but.innerText = "删除";
            //事件函数都是异步的,所以不用担心addLi还没有更新
            but.addEventListener("click", function () {
            //在这里会找到要删除元素的对应索引,并删除元素
              const index = addLi.indexOf(item);
              //addLi的元素被删除后,会再次触发这个set方法,然后ul被清空,再重新添加更新后addLi里现有的元素
              addLi.splice(index, 1);
            });
            //将删除按钮添加到li元素里
            li.appendChild(but);
            //将li添加到ul里
            ul.appendChild(li);
          });
        }
        return Reflect.set(...arguments);
      },
    });
   </script>

在对数组使用添加修改方法的时候,代理的set方法会执行两次或更多,这里打印一下日志可以看的更清楚

这样简单的toDoList 就利用Proxy简单的做好了,虽然看起来性能还并不是很好,因为再每次删除新增的时候都需要对dom进行大量的操作。对比真正的vue还需要很多工作要做。其实这里也可以看出来vue里key的作用很重要,在删除的时候,没有唯一标识,找不到被删除的那个元素,只能进行大量的dom操作来达到最终的呈现效果。

相关推荐
Mintopia6 分钟前
3D Quickhull 算法:用可见性与冲突图搭建空间凸壳
前端·javascript·计算机图形学
Mintopia6 分钟前
Three.js 三维数据交互与高并发优化:从点云到地图的底层修炼
前端·javascript·three.js
陌小路12 分钟前
5天 Vibe Coding 出一个在线音乐分享空间应用是什么体验
前端·aigc·vibecoding
成长ing1213820 分钟前
cocos creator 3.x shader 流光
前端·cocos creator
Alo36528 分钟前
antd 组件部分API使用方法
前端
BillKu31 分钟前
Vue3数组去重方法总结
前端·javascript·vue.js
GDAL33 分钟前
Object.freeze() 深度解析:不可变性的实现与实战指南
javascript·freeze
江城开朗的豌豆1 小时前
Vue+JSX真香现场:告别模板语法,解锁新姿势!
前端·javascript·vue.js
这里有鱼汤1 小时前
首个支持A股的AI多智能体金融系统,来了
前端·python
袁煦丞1 小时前
5分钟搭建高颜值后台!SoybeanAdmin:cpolar内网穿透实验室第648个成功挑战
前端·程序员·远程工作