Explain in detail defineProperty and Proxy (simple realization of two-way data binding)

Preface

The key to "data binding" is to monitor data changes. Vue data two-way binding is achieved through data hijacking combined with the publisher-subscriber model. In fact, the Object.defineProperty method in ES5 is mainly used to hijack the operation of adding or modifying the properties of the object, thereby updating the view.

I heard that vue3.0 will replace the Object.defineProperty() method with proxy. So it is necessary to understand some usages in advance. The proxy can directly hijack the entire object, not the properties of the object, and there are many ways to hijack it. And will eventually return to the new object after the hijacking. So relatively speaking, this method is quite easy to use. But the compatibility is not very good.

一、defineProperty

ES5 provides the Object.defineProperty method, which can define a new property on an object, or modify an existing property of an object, and return the object.

[1] Grammar

Object.defineProperty(obj, prop, descriptor)

parameter:

obj: required, target object

prop: required, the name of the property to be defined or modified

descriptor: required, the descriptor of the attribute to be defined or modified

return value:

The object passed into the function, that is, the first parameter obj

[2] Descriptor parameter analysis

The attribute descriptor represented by the third parameter descriptor of the function has two forms: data descriptor and access descriptor

Data description: When modifying or defining an attribute of an object, add some characteristics to the attribute, and the attributes in the data description are optional

  • value: the value corresponding to the property, which can be any type of value, the default is undefined
  • writable: Whether the value of the attribute can be rewritten. Set to true can be overridden; set to false, cannot be overridden. The default is false
  • enumerable: Whether this attribute can be enumerated (using for...in or Object.keys()). Set to true to be enumerated; set to false to not be enumerated. The default is false
  • configurable: Whether the target attribute can be deleted or whether the attributes (writable, configurable, enumerable) can be modified again. If set to true, the feature can be deleted or reset; if set to false, the feature cannot be deleted or reset. The default is false. This attribute plays two roles: 1. Whether the target attribute can be deleted using delete 2. Whether the target attribute can be set again

Access description: When using the accessor to describe the properties of an attribute, the following properties are allowed to be set

  • get: The getter function of the property, if there is no getter, it is undefined. When this property is accessed, this function is called. No parameters are passed in during execution, but the this object is passed in (due to inheritance, this here is not necessarily the object that defines the property). The return value of this function will be used as the value of the attribute. The default is undefined.
  • set: The setter function of the property, if there is no setter, it is undefined. When the attribute value is modified, this function will be called. This method accepts a parameter (that is, the new value to be assigned) and will pass in the this object at the time of assignment. The default is undefined.

[3] Example

  • 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 and 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... 张三

Two, Proxy 

The Proxy object is used to define the custom behavior of basic operations (such as attribute lookup, assignment, enumeration, function call, etc.).
In fact, it provides interception before the operation of the target object, and can filter and rewrite the external operation, and modify a certain The default behavior of these operations, so that we can not directly manipulate the object itself, but indirectly manipulate the object through the proxy object of the manipulated object to achieve the desired purpose~

[1] Grammar

const p = new Proxy(target, handler)

【2】Parameter

target: The target object to be wrapped by Proxy (it can be any type of object, including native arrays, functions, or even another proxy)

handler: It is also an object, and its attribute is a function that defines the behavior of the agent when an operation is performed, that is, a custom behavior

[3] Handler method

The handler object is a placeholder object that holds a batch of specific properties. It contains various traps of Proxy, all traps are optional. If a catcher is not defined, the default behavior of the source object will be retained.

handler.getPrototypeOf()  The catcher for the Object.getPrototypeOf method.
handler.setPrototypeOf()     The catcher for the Object.setPrototypeOf method.
handler.isExtensible () The catcher for the Object.isExtensible method.
handler.preventExtensions()  The catcher for the Object.preventExtensions method.

handler.getOwnPropertyDescriptor()  

A catcher for the Object.getOwnPropertyDescriptor method.
handler.defineProperty()     The catcher for the Object.defineProperty method.
handler.has()   The catcher for the in operator.
handler.get()    The catcher for attribute read operations.
handler.set()  The catcher for property setting operations.
handler.deleteProperty() The catcher for the delete operator.
handler.ownKeys ()  A catcher for the Object.getOwnPropertyNames method and the Object.getOwnPropertySymbols method.
handler.apply()  The catcher for function call operations.
handler.construct()  A catcher for the new operator.

[4] Example

  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... 张三

  

Three, defineProperty and Proxy comparison

  1. Object.defineProperty can only hijack the properties of the object, and Proxy is a direct proxy object.
    Since Object.defineProperty can only hijack properties, each property of the object needs to be traversed. If the property value is also an object, a deep traversal is required. And Proxy directly proxy objects, no traversal operation is required.

  2. Object.defineProperty requires manual Observe for new properties.
    Since Object.defineProperty hijacks the properties of the object, when adding properties, you need to re-traverse the object (changing the properties will not automatically trigger the setter), and then use Object.defineProperty to hijack the new properties.
    It is for this reason that when using vue to add attributes to arrays or objects in data, you need to use vm.$set to ensure that the added attributes are also responsive.

  3. defineProperty will pollute the original object (the key difference) The
    proxy proxy to ob, he will return a new proxy object without making changes to the original object ob, while defineproperty is to modify the meta object and modify the properties of the meta object, and the proxy is only for The meta-object proxies and gives a new proxy object.

Four, simply realize two-way data binding

[1] Create a new myVue.js file and create a myVue class

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] Introduce myVue.js in the html file and create an example

<!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】Effect

Articles are continuously updated every week. You can search for " Front-end Collection  " on WeChat to  read it for the first time, and reply to [ Video ] [ Book ] to receive 200G video materials and 30 PDF book materials

Guess you like

Origin blog.csdn.net/qq_38128179/article/details/111416502