1. 程式人生 > 程式設計 >ES6使用新特性Proxy實現的資料繫結功能例項

ES6使用新特性Proxy實現的資料繫結功能例項

本文例項講述了ES6使用新特性Proxy實現的資料繫結功能。分享給大家供大家參考,具體如下:

專案地址:https://github.com/jrainlau/mog
線上體驗:https://codepen.io/jrainlau/pen/YpyBBY


作為一個前端開發者,曾踩了太多的“資料繫結”的坑。在早些時候,都是通過jQuery之類的工具手動完成這些功能,但是當資料量非常大的時候,這些手動的工作讓我非常痛苦。直到使用了VueJS,這些痛苦才得以終結。

VueJS的其中一個賣點,就是“資料繫結”。使用者無需關心資料是怎麼繫結到dom上面的,只需要關注資料就好,因為VueJS已經自動幫我們完成了這些工作。

這真的非常神奇,我不可救藥地愛上了VueJS,並且把它用到我自己的專案當中。隨著使用的深入,我更加想知道它深入的原理是什麼。

VueJS是如何進行資料繫結的?

通過閱讀官方文件,我看到了下面這段話:

把一個普通 Javascript 物件傳給 Vue 例項來作為它的 data 選項,Vue 將遍歷它的屬性,用 Object.defineProperty 將它們轉為 getter/setter。

關鍵詞是Object.definProperty,在MDN文件裡面是這麼說的:

Object.defineProperty()方法直接定義一個物件的屬性,或者修改物件當中一個已經存在的屬性,並返回這個物件。

讓我們寫個例子來測試一下它。

首先,建立一個鋼鐵俠物件並賦予他一些屬性:

let ironman = {
 name: 'Tony Stark',sex: 'male',age: '35'
}

現在我們使用Object.defineProperty()方法來對他的一些屬性進行修改,並且在控制檯把所修改的內容輸出:

Object.defineProperty(ironman,'age',{
 set (val) {
  console.log(`Set age to ${val}`)
  return val
 }
})

ironman.age = '48'
// --> Set age to 48

看起來挺完美的。如果把console.log('Set age to ${val}')改為element.innerHTML = val,是不是就意味著資料繫結已經完成了呢?

讓我們再修改一下鋼鐵俠的屬性:

let ironman = {
 name: 'Tony Stark',age: '35',hobbies: ['girl','money','game']
}

嗯……他就是一個花花公子。現在我想把一些“愛好”新增到他身上,並且在控制檯看到對應的輸出:

Object.defineProperty(ironman.hobbies,'push',{
 value () {
  console.log(`Push ${arguments[0]} to ${this}`)
  this[this.length] = arguments[0]
 }
})

ironman.hobbies.push('wine')
console.log(ironman.hobbies)

// --> Push wine to girl,money,game
// --> [ 'girl','game','wine' ]

在此之前,我是使用get()方法去追蹤物件的屬性變化,但是對於一個數組,我們不能使用這個方法,而是使用value()方法來代替。雖然這招也靈,但是並非最好的辦法。有沒有更好的方法可以簡化這些追蹤物件或陣列屬性變化的方法呢?

在ECMA2015,Proxy是一個不錯的選擇

什麼是Proxy?在MDN文件中是這麼說的(誤):

Proxy可以理解成,在目標物件之前架設一層“攔截”,外界對該物件的訪問,都必須先通過這層攔截,因此提供了一種機制,可以對外界的訪問進行過濾和改寫。

Proxy是ECMA2015的一個新特性,它非常強大,但我並不會討論太多關於它的東西,除了我們現在需要的一個。現在讓我們一起來新建一個Proxy例項:

let ironmanProxy = new Proxy(ironman,{
 set (target,property,value) {
  target[property] = value
  console.log('change....')
  return true
 }
})

ironmanProxy.age = '48'
console.log(ironman.age)

// --> change....
// --> 48

符合預期。那麼對於陣列呢?

let ironmanProxy = new Proxy(ironman.hobbies,value) {
  target[property] = value
  console.log('change....')
  return true
 }
})

ironmanProxy.push('wine')
console.log(ironman.hobbies)

// --> change...
// --> change...
// --> [ 'girl','wine' ]

仍然符合預期!但是為什麼輸出了兩次change...呢?因為每當我觸發push()方法的時候,這個陣列的length屬性和body內容都被修改了,所以會引起兩次變化。

實時資料繫結

解決了最核心的問題,可以考慮其他的問題了。

想象一下,我們有一個模板和資料物件:

<!-- html template -->
<p>Hello,my name is {{name}},I enjoy eatting {{hobbies.food}}</p>

<!-- javascript -->
let ironman = {
 name: 'Tony Stark',hobbies: {
  food: 'banana',drink: 'wine'
 }
}

通過前面的程式碼,我們知道如果想要追蹤一個物件的屬性變化,我們應該把這個屬性作為第一個引數傳入Proxy例項。讓我們一起來建立一個返回新的Proxy例項的函式吧!

function $setData (dataObj,fn) {
  let self = this
  let once = false
  let $d = new Proxy(dataObj,{
   set (target,value) {
    if (!once) {
     target[property] = value
     once = true
     /* Do something here */
    }
    return true
   }
  })
  fn($d)
 }

它可以通過以下的方式被使用:

$setData(dataObj,($d) => {
 /* 
  * dataObj.someProps = something
  */
})

// 或者

$setData(dataObj.arrayProps,($d) => {
 /* 
  * dataObj.push(something)
  */
})

除此之外,我們應該實現模板對資料物件的對映,這樣才能用Tony Stark來替換{{name}}

function replaceFun(str,data) {
  let self = this
  return str.replace(/{{([^{}]*)}}/g,(a,b) => {
   return data[b]
  })
 }

replaceFun('My name is {{name}}',{ name: 'xxx' })
// --> My name is xxx

這個函式對於如{ name: 'xx',age: 18 }的單層屬性物件執行良好,但是對於如{ hobbies: { food: 'apple',drink: 'milk' } }這樣的多層屬性物件卻無能為力。舉個例子,如果模板關鍵字是{{hobbies.food}},那麼replaceFun()函式就應該返回data['hobbies']['food']

為了解決這個問題,再來一個函式:

function getObjProp (obj,propsName) {
  let propsArr = propsName.split('.')
  function rec(o,pName) {
   if (!o[pName] instanceof Array && o[pName] instanceof Object) {
    return rec(o[pName],propsArr.shift())
   }
   return o[pName]
  }
  return rec(obj,propsArr.shift())
 }

getObjProp({ data: { hobbies: { food: 'apple',drink: 'milk' } } },'hobbies.food')
// --> return { food: 'apple',drink: 'milk' }

最終的replaceFun()函式應該是下面這樣子的:

function replaceFun(str,b) => {
   let r = self._getObjProp(data,b);
   console.log(a,b,r)
   if (typeof r === 'string' || typeof r === 'number') {
    return r
   } else {
    return self._getObjProp(r,b.split('.')[1])
   }
  })
 }

一個數據繫結的例項,叫做“Mog”

不為什麼,就叫做“Mog”。

class Mog {
 constructor (options) {
  this.$data = options.data
  this.$el = options.el
  this.$tpl = options.template
  this._render(this.$tpl,this.$data)
 }

 $setData (dataObj,value) {
    if (!once) {
     target[property] = value
     once = true
     self._render(self.$tpl,self.$data)
    }
    return true
   }
  })
  fn($d)
 }

 _render (tplString,data) {
  document.querySelector(this.$el).innerHTML = this._replaceFun(tplString,data)
 }

 _replaceFun(str,b.split('.')[1])
   }
  })
 }

 _getObjProp (obj,propsArr.shift())
 }

}

使用:

<!-- html -->

  <div id="app">
   <p>
    Hello everyone,my name is <span>{{name}}</span>,I am a mini <span>{{lang}}</span> framework for just <span>{{work}}</span>. I can bind data from <span>{{supports.0}}</span>,<span>{{supports.1}}</span> and <span>{{supports.2}}</span>. What's more,I was created by <span>{{info.author}}</span>,and was written in <span>{{info.jsVersion}}</span>. My motto is "<span>{{motto}}</span>".
   </p>
  </div>
  <div id="input-wrapper">
   Motto: <input type="text" id="set-motto" autofocus>
  </div>
<!-- javascript -->

let template = document.querySelector('#app').innerHTML

let mog = new Mog({
 template: template,el: '#app',data: {
  name: 'mog',lang: 'javascript',work: 'data binding',supports: ['String','Array','Object'],info: {
   author: 'Jrain',jsVersion: 'Ecma2015'
  },motto: 'Every dog has his day'
 }
})

document.querySelector('#set-motto').oninput = (e) => {
 mog.$setData(mog.$data,($d) => {
  $d.motto = e.target.value
 })
}

你可以在這裡進行線上體驗。

後記

Mog僅僅是一個用於學習資料繫結的實驗性質的專案,程式碼仍然不夠優雅,功能也不夠豐富。但是這個小玩具讓我學習了很多。如果你對它有興趣,歡迎到這裡把專案fork走,並且加入一些你的想法。

感謝閱讀!

感興趣的朋友可以使用線上HTML/CSS/JavaScript程式碼執行工具:http://tools.jb51.net/code/HtmlJsRun測試上述程式碼執行效果。

更多關於JavaScript相關內容感興趣的讀者可檢視本站專題:《javascript面向物件入門教程》、《JavaScript錯誤與除錯技巧總結》、《JavaScript資料結構與演算法技巧總結》、《JavaScript遍歷演算法與技巧總結》及《JavaScript數學運算用法總結》

希望本文所述對大家JavaScript程式設計有所幫助。