美文网首页
详解defineProperty和Proxy (简单实现数据双向

详解defineProperty和Proxy (简单实现数据双向

作者: dingFY | 来源:发表于2020-12-20 18:08 被阅读0次

    前言

    "数据绑定" 的关键在于监听数据的变化,vue数据双向绑定是通过数据劫持结合发布者-订阅者模式的方式来实现的。其实主要是用了ES5中的Object.defineProperty方法来劫持对象的属性添加或修改的操作,从而更新视图。

    听说vue3.0 会用 proxy 替代 Object.defineProperty()方法。所以预先了解一些用法是有必要的。proxy 能够直接 劫持整个对象,而不是对象的属性,并且劫持的方法有多种。而且最后会返回劫持后的新对象。所以相对来讲,这个方法还是挺好用的。不过兼容性不太好。

    一、defineProperty

    ES5 提供了 Object.defineProperty 方法,该方法可以在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象。

    【1】语法

    Object.defineProperty(obj, prop, descriptor)
    

    参数:

    obj:必需,目标对象

    prop:必需,需定义或修改的属性的名字

    descriptor:必需,将被定义或修改的属性的描述符

    返回值:

    传入函数的对象,即第一个参数obj

    【2】descriptor参数解析

    函数的第三个参数 descriptor 所表示的属性描述符有两种形式:数据描述符和存取描述符

    数据描述:当修改或定义对象的某个属性的时候,给这个属性添加一些特性,数据描述中的属性都是可选的

    • value:属性对应的值,可以使任意类型的值,默认为undefined
    • writable:属性的值是否可以被重写。设置为true可以被重写;设置为false,不能被重写。默认为false
    • enumerable:此属性是否可以被枚举(使用for...in或Object.keys())。设置为true可以被枚举;设置为false,不能被枚举。默认为false
    • configurable:是否可以删除目标属性或是否可以再次修改属性的特性(writable, configurable, enumerable)。设置为true可以被删除或可以重新设置特性;设置为false,不能被可以被删除或不可以重新设置特性。默认为false。这个属性起到两个作用:1、目标属性是否可以使用delete删除 2、目标属性是否可以再次设置特性

    存取描述:当使用存取器描述属性的特性的时候,允许设置以下特性属性

    • get:属性的 getter 函数,如果没有 getter,则为 undefined。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。默认为 undefined。
    • set:属性的 setter 函数,如果没有 setter,则为 undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。默认为 undefined。

    【3】示例

    • value
      let obj = {}
      // 不设置value属性
      Object.defineProperty(obj, "name", {});
      console.log(obj.name); // undefined
    
      // 设置value属性
      Object.defineProperty(obj, "name", {
        value: "Demi"
      });
      console.log(obj.name); // Demi
    
    • writable
      let obj = {}
      // writable设置为false,不能重写
      Object.defineProperty(obj, "name", {
        value: "Demi",
        writable: false
      });
      //更改name的值(更改失败)
      obj.name = "张三";
      console.log(obj.name); // Demi 
    
      // writable设置为true,可以重写
      Object.defineProperty(obj, "name", {
        value: "Demi",
        writable: true
      });
      //更改name的值
      obj.name = "张三";
      console.log(obj.name); // 张三 
    
    • enumerable
      let obj = {}
      // enumerable设置为false,不能被枚举。
      Object.defineProperty(obj, "name", {
        value: "Demi",
        writable: false,
        enumerable: false
      });
    
      // 枚举对象的属性
      for (let attr in obj) {
        console.log(attr);
      }
    
      // enumerable设置为true,可以被枚举。
      Object.defineProperty(obj, "age", {
        value: 18,
        writable: false,
        enumerable: true
      });
    
      // 枚举对象的属性
      for (let attr in obj) {
        console.log(attr); //age
      }
    
    • **configurable **
      //-----------------测试目标属性是否能被删除------------------------//
      let obj = {}
      // configurable设置为false,不能被删除。
      Object.defineProperty(obj, "name", {
        value: "Demi",
        writable: false,
        enumerable: false,
        configurable: false
      });
      // 删除属性
      delete obj.name;
      console.log(obj.name); // Demi
    
      // configurable设置为true,可以被删除。
      Object.defineProperty(obj, "age", {
        value: 19,
        writable: false,
        enumerable: false,
        configurable: true
      });
      // 删除属性
      delete obj.age;
      console.log(obj.age); // undefined
    
      //-----------------测试是否可以再次修改特性------------------------//
      let obj2 = {}
      // configurable设置为false,不能再次修改特性。
      Object.defineProperty(obj2, "name", {
        value: "dingFY",
        writable: false,
        enumerable: false,
        configurable: false
      });
    
      //重新修改特性
      Object.defineProperty(obj2, "name", {
          value: "张三",
          writable: true,
          enumerable: true,
          configurable: true
      });
      console.log(obj2.name); // 报错:Uncaught TypeError: Cannot redefine property: name
    
      // configurable设置为true,可以再次修改特性。
      Object.defineProperty(obj2, "age", {
        value: 18,
        writable: false,
        enumerable: false,
        configurable: true
      });
    
      // 重新修改特性
      Object.defineProperty(obj2, "age", {
        value: 20,
        writable: true,
        enumerable: true,
        configurable: true
      });
      console.log(obj2.age); // 20
    
    • set 和 get
      let obj = {
        name: 'Demi'
      };
      Object.defineProperty(obj, "name", {
        get: function () {
          //当获取值的时候触发的函数
          console.log('get...')
        },
        set: function (newValue) {
          //当设置值的时候触发的函数,设置的新值通过参数value拿到
          console.log('set...', newValue)
        }
      });
    
      //获取值
      obj.name // get...
    
      //设置值
      obj.name = '张三'; // set... 张三
    

    二、Proxy

    Proxy 对象用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)
    其实就是在对目标对象的操作之前提供了拦截,可以对外界的操作进行过滤和改写,修改某些操作的默认行为,这样我们可以不直接操作对象本身,而是通过操作对象的代理对象来间接来操作对象,达到预期的目的~

    【1】语法

    const p = new Proxy(target, handler)
    

    【2】参数

    target:要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)

    handler:也是一个对象,其属性是当执行一个操作时定义代理的行为的函数,也就是自定义的行为

    【3】handler方法

    handler 对象是一个容纳一批特定属性的占位符对象。它包含有 Proxy 的各个捕获器(trap),所有的捕捉器是可选的。如果没有定义某个捕捉器,那么就会保留源对象的默认行为。

    handler.getPrototypeOf()  ===》  Object.getPrototypeOf 方法的捕捉器
    handler.setPrototypeOf()     ===》  Object.setPrototypeOf 方法的捕捉器
    handler.isExtensible() ===》  Object.isExtensible 方法的捕捉器
    handler.preventExtensions()  ===》  Object.preventExtensions 方法的捕捉器
    handler.getOwnPropertyDescriptor() ===》  Object.getOwnPropertyDescriptor 方法的捕捉器
    handler.defineProperty()     ===》  Object.defineProperty 方法的捕捉器
    handler.has()   ===》  in 操作符的捕捉器
    handler.get()    ===》  属性读取操作的捕捉器
    handler.set()  ===》  属性设置操作的捕捉器
    handler.deleteProperty() ===》  delete 操作符的捕捉器
    handler.ownKeys()  ===》  Object.getOwnPropertyNames方法和 Object.getOwnPropertySymbols 方法的捕捉器
    handler.apply()  ===》  函数调用操作的捕捉器
    handler.construct()  ===》  new 操作符的捕捉器
    

    【4】示例

      let obj = {
        name: 'name',
        age: 18
      }
    
      let p = new Proxy(obj, {
        get: function (target, property, receiver) {
          console.log('get...')
        },
        set: function (target, property, value, receiver) {
          console.log('set...', value)
        }
      })
    
      p.name // get...
      p = {
        name: 'dingFY',
        age: 20
      }
      // p.name = '张三' // set... 张三
    
    

    三、defineProperty和Proxy对比

    1. Object.defineProperty只能劫持对象的属性,而Proxy是直接代理对象。
      由于 Object.defineProperty 只能对属性进行劫持,需要遍历对象的每个属性,如果属性值也是对象,则需要深度遍历。而 Proxy 直接代理对象,不需要遍历操作。
    1. Object.defineProperty对新增属性需要手动进行Observe。
      由于 Object.defineProperty 劫持的是对象的属性,所以新增属性时,需要重新遍历对象(改变属性不会自动触发setter),对其新增属性再使用 Object.defineProperty 进行劫持。
      也正是因为这个原因,使用vue给 data 中的数组或对象新增属性时,需要使用 vm.$set 才能保证新增的属性也是响应式的。
    1. defineProperty会污染原对象(关键区别)
      proxy去代理了ob,他会返回一个新的代理对象不会对原对象ob进行改动,而defineproperty是去修改元对象,修改元对象的属性,而proxy只是对元对象进行代理并给出一个新的代理对象。

    四、简单实现数据双向绑定

    【1】新建myVue.js文件,创建myVue类

    class myVue extends EventTarget {
      constructor(options) {
        super();
        this.$options = options;
        this.compile();
        this.observe(this.$options.data);
      }
    
      // 数据劫持
      observe(data) {
        let keys = Object.keys(data);
        // 遍历循环data数据,给每个属性增加数据劫持
        keys.forEach(key => {
          this.defineReact(data, key, data[key]);
        })
      }
    
      // 利用defineProperty 进行数据劫持
      defineReact(data, key, value) {
        let _this = this;
        Object.defineProperty(data, key, {
          configurable: true,
          enumerable: true,
          get() {
            return value;
          },
          set(newValue) {
            // 监听到数据变化, 触发事件
            let event = new CustomEvent(key, {
              detail: newValue
            });
            _this.dispatchEvent(event);
            value = newValue;
          }
        });
      }
    
      // 获取元素节点,渲染视图
      compile() {
        let el = document.querySelector(this.$options.el);
        this.compileNode(el);
      }
      // 渲染视图
      compileNode(el) {
        let childNodes = el.childNodes;
        // 遍历循环所有元素节点
        childNodes.forEach(node => {
          if (node.nodeType === 1) {
            // 如果是标签 需要跟进元素attribute 属性区分v-html 和 v-model
            let attrs = node.attributes;
            [...attrs].forEach(attr => {
              let attrName = attr.name;
              let attrValue = attr.value;
              if (attrName.indexOf("v-") === 0) {
                attrName = attrName.substr(2);
                // 如果是 html 直接替换为将节点的innerHTML替换成data数据
                if (attrName === "html") {
                  node.innerHTML = this.$options.data[attrValue];
                } else if (attrName === "model") {
                  // 如果是 model 需要将input的value值替换成data数据
                  node.value = this.$options.data[attrValue];
    
                  // 监听input数据变化,改变data值
                  node.addEventListener("input", e => {
                    this.$options.data[attrValue] = e.target.value;
                  })
                }
              }
            })
            if (node.childNodes.length > 0) {
              this.compileNode(node);
            }
          } else if (node.nodeType === 3) {
            // 如果是文本节点, 直接利用正则匹配到文本节点的内容,替换成data的内容
            let reg = /\{\{\s*(\S+)\s*\}\}/g;
            let textContent = node.textContent;
            if (reg.test(textContent)) {
              let $1 = RegExp.$1;
              node.textContent = node.textContent.replace(reg, this.$options.data[$1]);
              // 监听数据变化,重新渲染视图
              this.addEventListener($1, e => {
                let oldValue = this.$options.data[$1];
                let reg = new RegExp(oldValue);
                node.textContent = node.textContent.replace(reg, e.detail);
              })
            }
          }
        })
      }
    }
    

    【2】在html文件中引入myVue.js, 创建实例

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <meta http-equiv="X-UA-Compatible" content="ie=edge">
      <script src="./mvvm.js" type="text/javascript"></script>
      <title>Document</title>
    </head>
    
    <body>
      <div id="app">
        <div>我的名字叫:{{name}}</div>
        <div v-html="htmlData"></div>
        <input v-model="modelData" /> {{modelData}}
      </div>
    
    </body>
    <script>
      let vm = new myVue({
        el: "#app",
        data: {
          name: "Demi",
          htmlData: "html数据",
          modelData: "input的数据"
        }
      })
    </script>
    
    </html>
    

    【3】效果

    文章每周持续更新,可以微信搜索「 前端大集锦 」第一时间阅读,回复【视频】【书籍】领取200G视频资料和30本PDF书籍资料

    相关文章

      网友评论

          本文标题:详解defineProperty和Proxy (简单实现数据双向

          本文链接:https://www.haomeiwen.com/subject/ijpknktx.html