詳解defineProperty和Proxy (簡(jiǎn)單實(shí)現(xiàn)數(shù)據(jù)雙向綁定)

前言

"數(shù)據(jù)綁定" 的關(guān)鍵在于監(jiān)聽(tīng)數(shù)據(jù)的變化粟矿,vue數(shù)據(jù)雙向綁定是通過(guò)數(shù)據(jù)劫持結(jié)合發(fā)布者-訂閱者模式的方式來(lái)實(shí)現(xiàn)的。其實(shí)主要是用了ES5中的Object.defineProperty方法來(lái)劫持對(duì)象的屬性添加或修改的操作键袱,從而更新視圖侮穿。

聽(tīng)說(shuō)vue3.0 會(huì)用 proxy 替代 Object.defineProperty()方法薪伏。所以預(yù)先了解一些用法是有必要的播玖。proxy 能夠直接 劫持整個(gè)對(duì)象,而不是對(duì)象的屬性,并且劫持的方法有多種洛二。而且最后會(huì)返回劫持后的新對(duì)象。所以相對(duì)來(lái)講攻锰,這個(gè)方法還是挺好用的晾嘶。不過(guò)兼容性不太好。

一娶吞、defineProperty

ES5 提供了 Object.defineProperty 方法垒迂,該方法可以在一個(gè)對(duì)象上定義一個(gè)新屬性,或者修改一個(gè)對(duì)象的現(xiàn)有屬性妒蛇,并返回這個(gè)對(duì)象机断。

【1】語(yǔ)法

Object.defineProperty(obj, prop, descriptor)

參數(shù):

obj:必需,目標(biāo)對(duì)象

prop:必需绣夺,需定義或修改的屬性的名字

descriptor:必需吏奸,將被定義或修改的屬性的描述符

返回值:

傳入函數(shù)的對(duì)象,即第一個(gè)參數(shù)obj

【2】descriptor參數(shù)解析

函數(shù)的第三個(gè)參數(shù) descriptor 所表示的屬性描述符有兩種形式:數(shù)據(jù)描述符和存取描述符

數(shù)據(jù)描述:當(dāng)修改或定義對(duì)象的某個(gè)屬性的時(shí)候陶耍,給這個(gè)屬性添加一些特性奋蔚,數(shù)據(jù)描述中的屬性都是可選的

  • value:屬性對(duì)應(yīng)的值,可以使任意類型的值,默認(rèn)為undefined
  • writable:屬性的值是否可以被重寫。設(shè)置為true可以被重寫泊碑;設(shè)置為false坤按,不能被重寫。默認(rèn)為false
  • enumerable:此屬性是否可以被枚舉(使用for...in或Object.keys())馒过。設(shè)置為true可以被枚舉臭脓;設(shè)置為false,不能被枚舉沉桌。默認(rèn)為false
  • configurable:是否可以刪除目標(biāo)屬性或是否可以再次修改屬性的特性(writable, configurable, enumerable)谢鹊。設(shè)置為true可以被刪除或可以重新設(shè)置特性;設(shè)置為false留凭,不能被可以被刪除或不可以重新設(shè)置特性佃扼。默認(rèn)為false。這個(gè)屬性起到兩個(gè)作用:1蔼夜、目標(biāo)屬性是否可以使用delete刪除 2兼耀、目標(biāo)屬性是否可以再次設(shè)置特性

存取描述:當(dāng)使用存取器描述屬性的特性的時(shí)候,允許設(shè)置以下特性屬性

  • get:屬性的 getter 函數(shù)求冷,如果沒(méi)有 getter瘤运,則為 undefined。當(dāng)訪問(wèn)該屬性時(shí)匠题,會(huì)調(diào)用此函數(shù)拯坟。執(zhí)行時(shí)不傳入任何參數(shù),但是會(huì)傳入 this 對(duì)象(由于繼承關(guān)系韭山,這里的this并不一定是定義該屬性的對(duì)象)郁季。該函數(shù)的返回值會(huì)被用作屬性的值。默認(rèn)為 undefined钱磅。
  • set:屬性的 setter 函數(shù)梦裂,如果沒(méi)有 setter,則為 undefined盖淡。當(dāng)屬性值被修改時(shí)年柠,會(huì)調(diào)用此函數(shù)。該方法接受一個(gè)參數(shù)(也就是被賦予的新值)褪迟,會(huì)傳入賦值時(shí)的 this 對(duì)象冗恨。默認(rèn)為 undefined。

【3】示例

  • value
  let obj = {}
  // 不設(shè)置value屬性
  Object.defineProperty(obj, "name", {});
  console.log(obj.name); // undefined

  // 設(shè)置value屬性
  Object.defineProperty(obj, "name", {
    value: "Demi"
  });
  console.log(obj.name); // Demi
  • writable
  let obj = {}
  // writable設(shè)置為false味赃,不能重寫
  Object.defineProperty(obj, "name", {
    value: "Demi",
    writable: false
  });
  //更改name的值(更改失斉山)
  obj.name = "張三";
  console.log(obj.name); // Demi 

  // writable設(shè)置為true,可以重寫
  Object.defineProperty(obj, "name", {
    value: "Demi",
    writable: true
  });
  //更改name的值
  obj.name = "張三";
  console.log(obj.name); // 張三 
  • enumerable
  let obj = {}
  // enumerable設(shè)置為false洁桌,不能被枚舉。
  Object.defineProperty(obj, "name", {
    value: "Demi",
    writable: false,
    enumerable: false
  });

  // 枚舉對(duì)象的屬性
  for (let attr in obj) {
    console.log(attr);
  }

  // enumerable設(shè)置為true侯嘀,可以被枚舉另凌。
  Object.defineProperty(obj, "age", {
    value: 18,
    writable: false,
    enumerable: true
  });

  // 枚舉對(duì)象的屬性
  for (let attr in obj) {
    console.log(attr); //age
  }
  • **configurable **
  //-----------------測(cè)試目標(biāo)屬性是否能被刪除------------------------//
  let obj = {}
  // configurable設(shè)置為false谱轨,不能被刪除。
  Object.defineProperty(obj, "name", {
    value: "Demi",
    writable: false,
    enumerable: false,
    configurable: false
  });
  // 刪除屬性
  delete obj.name;
  console.log(obj.name); // Demi

  // configurable設(shè)置為true吠谢,可以被刪除土童。
  Object.defineProperty(obj, "age", {
    value: 19,
    writable: false,
    enumerable: false,
    configurable: true
  });
  // 刪除屬性
  delete obj.age;
  console.log(obj.age); // undefined

  //-----------------測(cè)試是否可以再次修改特性------------------------//
  let obj2 = {}
  // configurable設(shè)置為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); // 報(bào)錯(cuò):Uncaught TypeError: Cannot redefine property: name

  // configurable設(shè)置為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 () {
      //當(dāng)獲取值的時(shí)候觸發(fā)的函數(shù)
      console.log('get...')
    },
    set: function (newValue) {
      //當(dāng)設(shè)置值的時(shí)候觸發(fā)的函數(shù),設(shè)置的新值通過(guò)參數(shù)value拿到
      console.log('set...', newValue)
    }
  });

  //獲取值
  obj.name // get...

  //設(shè)置值
  obj.name = '張三'; // set... 張三

二王污、Proxy

Proxy 對(duì)象用于定義基本操作的自定義行為(如屬性查找罢吃,賦值,枚舉昭齐,函數(shù)調(diào)用等)
其實(shí)就是在對(duì)目標(biāo)對(duì)象的操作之前提供了攔截尿招,可以對(duì)外界的操作進(jìn)行過(guò)濾和改寫,修改某些操作的默認(rèn)行為阱驾,這樣我們可以不直接操作對(duì)象本身就谜,而是通過(guò)操作對(duì)象的代理對(duì)象來(lái)間接來(lái)操作對(duì)象,達(dá)到預(yù)期的目的~

【1】語(yǔ)法

const p = new Proxy(target, handler)

【2】參數(shù)

target:要使用 Proxy 包裝的目標(biāo)對(duì)象(可以是任何類型的對(duì)象里覆,包括原生數(shù)組丧荐,函數(shù),甚至另一個(gè)代理)

handler:也是一個(gè)對(duì)象喧枷,其屬性是當(dāng)執(zhí)行一個(gè)操作時(shí)定義代理的行為的函數(shù)虹统,也就是自定義的行為

【3】handler方法

handler 對(duì)象是一個(gè)容納一批特定屬性的占位符對(duì)象。它包含有 Proxy 的各個(gè)捕獲器(trap)割去,所有的捕捉器是可選的窟却。如果沒(méi)有定義某個(gè)捕捉器,那么就會(huì)保留源對(duì)象的默認(rèn)行為呻逆。

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()  ===》  屬性設(shè)置操作的捕捉器
handler.deleteProperty() ===》  delete 操作符的捕捉器
handler.ownKeys()  ===》  Object.getOwnPropertyNames方法和 Object.getOwnPropertySymbols 方法的捕捉器
handler.apply()  ===》  函數(shù)調(diào)用操作的捕捉器
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對(duì)比

  1. Object.defineProperty只能劫持對(duì)象的屬性,而Proxy是直接代理對(duì)象咖城。
    由于 Object.defineProperty 只能對(duì)屬性進(jìn)行劫持茬腿,需要遍歷對(duì)象的每個(gè)屬性,如果屬性值也是對(duì)象宜雀,則需要深度遍歷切平。而 Proxy 直接代理對(duì)象,不需要遍歷操作辐董。
  1. Object.defineProperty對(duì)新增屬性需要手動(dòng)進(jìn)行Observe悴品。
    由于 Object.defineProperty 劫持的是對(duì)象的屬性,所以新增屬性時(shí),需要重新遍歷對(duì)象(改變屬性不會(huì)自動(dòng)觸發(fā)setter)苔严,對(duì)其新增屬性再使用 Object.defineProperty 進(jìn)行劫持定枷。
    也正是因?yàn)檫@個(gè)原因,使用vue給 data 中的數(shù)組或?qū)ο笮略鰧傩詴r(shí)届氢,需要使用 vm.$set 才能保證新增的屬性也是響應(yīng)式的欠窒。
  1. defineProperty會(huì)污染原對(duì)象(關(guān)鍵區(qū)別)
    proxy去代理了ob,他會(huì)返回一個(gè)新的代理對(duì)象不會(huì)對(duì)原對(duì)象ob進(jìn)行改動(dòng)退子,而defineproperty是去修改元對(duì)象岖妄,修改元對(duì)象的屬性,而proxy只是對(duì)元對(duì)象進(jìn)行代理并給出一個(gè)新的代理對(duì)象寂祥。

四荐虐、簡(jiǎn)單實(shí)現(xiàn)數(shù)據(jù)雙向綁定

【1】新建myVue.js文件,創(chuàng)建myVue類

class myVue extends EventTarget {
  constructor(options) {
    super();
    this.$options = options;
    this.compile();
    this.observe(this.$options.data);
  }

  // 數(shù)據(jù)劫持
  observe(data) {
    let keys = Object.keys(data);
    // 遍歷循環(huán)data數(shù)據(jù)壤靶,給每個(gè)屬性增加數(shù)據(jù)劫持
    keys.forEach(key => {
      this.defineReact(data, key, data[key]);
    })
  }

  // 利用defineProperty 進(jìn)行數(shù)據(jù)劫持
  defineReact(data, key, value) {
    let _this = this;
    Object.defineProperty(data, key, {
      configurable: true,
      enumerable: true,
      get() {
        return value;
      },
      set(newValue) {
        // 監(jiān)聽(tīng)到數(shù)據(jù)變化缚俏, 觸發(fā)事件
        let event = new CustomEvent(key, {
          detail: newValue
        });
        _this.dispatchEvent(event);
        value = newValue;
      }
    });
  }

  // 獲取元素節(jié)點(diǎn),渲染視圖
  compile() {
    let el = document.querySelector(this.$options.el);
    this.compileNode(el);
  }
  // 渲染視圖
  compileNode(el) {
    let childNodes = el.childNodes;
    // 遍歷循環(huán)所有元素節(jié)點(diǎn)
    childNodes.forEach(node => {
      if (node.nodeType === 1) {
        // 如果是標(biāo)簽 需要跟進(jìn)元素attribute 屬性區(qū)分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 直接替換為將節(jié)點(diǎn)的innerHTML替換成data數(shù)據(jù)
            if (attrName === "html") {
              node.innerHTML = this.$options.data[attrValue];
            } else if (attrName === "model") {
              // 如果是 model 需要將input的value值替換成data數(shù)據(jù)
              node.value = this.$options.data[attrValue];

              // 監(jiān)聽(tīng)input數(shù)據(jù)變化贮乳,改變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) {
        // 如果是文本節(jié)點(diǎn)忧换, 直接利用正則匹配到文本節(jié)點(diǎn)的內(nèi)容,替換成data的內(nèi)容
        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]);
          // 監(jiān)聽(tīng)數(shù)據(jù)變化向拆,重新渲染視圖
          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亚茬, 創(chuàng)建實(shí)例

<!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數(shù)據(jù)",
      modelData: "input的數(shù)據(jù)"
    }
  })
</script>

</html>

【3】效果

文章每周持續(xù)更新,可以微信搜索「 前端大集錦 」第一時(shí)間閱讀浓恳,回復(fù)【視頻】【書(shū)籍】領(lǐng)取200G視頻資料和30本PDF書(shū)籍資料

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末刹缝,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子颈将,更是在濱河造成了極大的恐慌梢夯,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,496評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件晴圾,死亡現(xiàn)場(chǎng)離奇詭異颂砸,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)死姚,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,407評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門人乓,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人都毒,你說(shuō)我怎么就攤上這事色罚。” “怎么了账劲?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,632評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵戳护,是天一觀的道長(zhǎng)金抡。 經(jīng)常有香客問(wèn)我,道長(zhǎng)姑尺,這世上最難降的妖魔是什么竟终? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,180評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮切蟋,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘榆芦。我一直安慰自己柄粹,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,198評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布匆绣。 她就那樣靜靜地躺著驻右,像睡著了一般。 火紅的嫁衣襯著肌膚如雪崎淳。 梳的紋絲不亂的頭發(fā)上堪夭,一...
    開(kāi)封第一講書(shū)人閱讀 51,165評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音拣凹,去河邊找鬼森爽。 笑死,一個(gè)胖子當(dāng)著我的面吹牛嚣镜,可吹牛的內(nèi)容都是我干的爬迟。 我是一名探鬼主播,決...
    沈念sama閱讀 40,052評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼菊匿,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼付呕!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起跌捆,我...
    開(kāi)封第一講書(shū)人閱讀 38,910評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤徽职,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后佩厚,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體姆钉,經(jīng)...
    沈念sama閱讀 45,324評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,542評(píng)論 2 332
  • 正文 我和宋清朗相戀三年可款,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了育韩。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,711評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡闺鲸,死狀恐怖筋讨,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情摸恍,我是刑警寧澤悉罕,帶...
    沈念sama閱讀 35,424評(píng)論 5 343
  • 正文 年R本政府宣布赤屋,位于F島的核電站,受9級(jí)特大地震影響壁袄,放射性物質(zhì)發(fā)生泄漏类早。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,017評(píng)論 3 326
  • 文/蒙蒙 一嗜逻、第九天 我趴在偏房一處隱蔽的房頂上張望涩僻。 院中可真熱鬧,春花似錦栈顷、人聲如沸逆日。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,668評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)室抽。三九已至,卻和暖如春靡努,著一層夾襖步出監(jiān)牢的瞬間坪圾,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,823評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工惑朦, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留兽泄,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,722評(píng)論 2 368
  • 正文 我出身青樓行嗤,卻偏偏與公主長(zhǎng)得像已日,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子栅屏,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,611評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容