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

前言

"数据绑定" 的关键在于监听数据的变化,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 直接代理对象,不需要遍历操作。

  2. Object.defineProperty对新增属性需要手动进行Observe。
    由于 Object.defineProperty 劫持的是对象的属性,所以新增属性时,需要重新遍历对象(改变属性不会自动触发setter),对其新增属性再使用 Object.defineProperty 进行劫持。
    也正是因为这个原因,使用vue给 data 中的数组或对象新增属性时,需要使用 vm.$set 才能保证新增的属性也是响应式的。

  3. 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书籍资料

猜你喜欢

转载自blog.csdn.net/qq_38128179/article/details/111416502