使用ES6的新特性Proxy來實現一個資料繫結例項

Jrain發表於2019-01-08

寫於 2016.10.11

專案地址:github.com/jrainlau/mo…線上體驗:codepen.io/jrainlau/pe…


作為一個前端開發者,曾踩了太多的“資料繫結”的坑。在早些時候,都是通過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', sex: 'male', 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, { 
set (target, property, value) {
target[property] = value console.log('change....') return true
}
})ironmanProxy.push('wine')console.log(ironman.hobbies)// -->
change...// -->
change...// -->
[ 'girl', 'money', 'game', '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', sex: 'male', age: '35', 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) { 
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, data) { 
let self = this return str.replace(/{{([^{
}]*)
}
}/g, (a, 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, fn) {
let self = this let once = false let $d = new Proxy(dataObj, {
set (target, property, 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, data) {
let self = this return str.replace(/{{([^{
}]*)
}
}/g, (a, 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])
}
})
} _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())
}
}複製程式碼

使用:

<
!-- 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').innerHTMLlet 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走,並且加入一些你的想法。

感謝閱讀!

來源:https://juejin.im/post/5c34a86a6fb9a049f81976d8

相關文章