不会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操作来达到最终的呈现效果。

相关推荐
ylfhpy14 分钟前
Python常见面试题的详解16
开发语言·python·面试
不能只会打代码18 分钟前
六十天前端强化训练之第一天HTML5语义化标签深度解析与博客搭建实战
前端·html·html5
OpenTiny社区35 分钟前
Node.js技术原理分析系列——Node.js的perf_hooks模块作用和用法
前端·node.js
菲力蒲LY39 分钟前
输入搜索、分组展示选项、下拉选取,全局跳转页,el-select 实现 —— 后端数据处理代码,抛砖引玉展思路
java·前端·mybatis
MickeyCV2 小时前
Nginx学习笔记:常用命令&端口占用报错解决&Nginx核心配置文件解读
前端·nginx
祈澈菇凉2 小时前
webpack和grunt以及gulp有什么不同?
前端·webpack·gulp
十步杀一人_千里不留行2 小时前
React Native 下拉选择组件首次点击失效问题的深入分析与解决
javascript·react native·react.js
zy0101012 小时前
HTML列表,表格和表单
前端·html
初辰ge2 小时前
【p-camera-h5】 一款开箱即用的H5相机插件,支持拍照、录像、动态水印与样式高度定制化。
前端·相机
HugeYLH3 小时前
解决npm问题:错误的代理设置
前端·npm·node.js