详解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 直接代理对象,不需要遍历操作。
  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书籍资料

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,393评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,790评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,391评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,703评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,613评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,003评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,507评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,158评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,300评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,256评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,274评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,984评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,569评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,662评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,899评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,268评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,840评论 2 339

推荐阅读更多精彩内容