天天看點

使用ES6的新特性Proxy來實作一個資料綁定執行個體

使用ES6的新特性Proxy來實作一個資料綁定執行個體

作為一個前端開發者,曾踩了太多的“資料綁定”的坑。在早些時候,都是通過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,是不是就意味着資料綁定已經完成了呢?

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

  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', 'money', '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, { 

ironmanproxy.push('wine') 

// --> change... 

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

實時資料綁定

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

想象一下,我們有一個模闆和資料對象:

<!-- html template --> 

<p>hello, my name is {{name}}, i enjoy eatting {{hobbies.food}}</p> 

<!-- javascript --> 

  hobbies: { 

    food: 'banana', 

    drink: 'wine' 

通過前面的代碼,我們知道如果想要追蹤一個對象的屬性變化,我們應該把這個屬性作為第一個參數傳入proxy執行個體。讓我們一起來建立一個傳回新的proxy執行個體的函數吧!

function $setdata (dataobj, fn) { 

    let self = this 

    let once = false 

    let $d = new proxy(dataobj, { 

      set (target, property, 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) { 

    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()函數應該是下面這樣子的:

      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, fn) { 

          self._render(self.$tpl, self.$data) 

  _render (tplstring, data) { 

    document.queryselector(this.$el).innerhtml = this._replacefun(tplstring, data) 

  _replacefun(str, data) { 

  _getobjprop (obj, propsname) { 

使用:

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

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走,并且加入一些你的想法。

感謝閱讀!

作者:jrainlau

來源:51cto

繼續閱讀