---恢復內容開始---
動態數據綁定是MVVM框架中最基礎的的一個功能,簡單描述就是:將數據和視圖進行綁定,當數據發生改變時,視圖隨之改變,更深層次一點,數據綁定包括單向數據綁定和雙向數據綁定。
本文從數據綁定中的問題出發,一步一步的來實現這個功能。
本文的所有的源代碼地址: 點擊此處查看源代碼
問題一
給定任意一個對象,如何監聽其屬性的讀取與變化?也就是說,如何知道程序訪問了對象的哪個屬性,又改變了哪個屬性?
舉個例子:
let app = new Observer({
name: 'liujianhuan', company: 'Qihoo 360', address: 'Chaoyang, Beijing' }) //要實現的結果如下 app.data.name //你訪問了name app.data.company //你訪問了company app.data.address = 'Beijing' //你設置了address, 新的值為 Beijing
實現這樣的一個Observer並不難,在此我們暫且不考慮數組的情況,只針對傳入的參數為對象。如果對ES6和ES5都熟悉的話,可以立刻想到針對上述場景,可以有兩種的實現方式:
- 采用ES6中的proxy,對目標對象的屬性進行攔截處理
- 采用ES5中的defineProperty,為目標對象的屬性添加setter和getter
接下來首先采用ES6中的proxy方法實現上述場景,首先從阮一峰老師的《es6入門標准中》摘錄:
Proxy可以理解成在目標對象前架設一“攔截”層,外界對該對象的訪問都必須先通過這層攔截,因此提供了一種機制,可以對外界的訪問進行過濾和改寫。Proxy這個詞的原意是代理,用在這里表示由它來“代理”某些操作,可以稱為“代理器”。
上邊的話讀完后應該和沒讀一樣,放出來也只是用來裝一下的。下邊直接用簡單的例子來說明:
ES6原生提供Proxy
構造函數,用於生成Proxy
實例。
var proxy = new Proxy(target,handler); var proxy = new Proxy(target,handler);
Proxy
對象的所有用法都是上面的形式,不同的只是handler
參數的寫法。其中new Proxy()
表示生成的一個Proxy
實例,target
參數表示所要攔截的目標對象,handler
參數也是一個對象,用來定制攔截行為。
下面來一個攔截讀取屬性行為的例子:
var proxy = new Proxy({},{ get:function(target,property){ return 35; } }); proxy.time;//35 proxy.name;//35 proxy.title;//35
上面代碼中,作為構造函數,Proxy
接受兩個參數。第一個參數是所要代理的目標對象(上例是一個空對象),即如果沒有Proxy
的介入,操作原來要訪問的就是這個對象;第二個參數是一個配置對象,對於每一個被代理的操作,需要提供一個對應的處理函數,該函數將攔截對應的操作。比如,上面代碼中,配置對象有一個get
方法,用來攔截對目標對象屬性的訪問請求。get
方法的兩個參數分別是目標對象和所要訪問的屬性。可以看到,由於攔截函數總是返回35,所以訪問任何屬性都得到35。
聽話分割線出來了,以上內容摘自《ES6標准入門(第二版)》,特此聲明!
看了上述的代碼之后,我想也應該不用再太多的介紹了,直接上針對問題一的代碼:
function Observer(data){ return new Proxy(data, { get: function(target, key){ if(key in target){ console.log('你訪問了' + key); return target[key]; }else{ throw new Error('key does not exist') } }, set: function(target, key, newVal){ console.log('你設置了' + key); console.log('新的' + key + '=' + newVal); target[key] = newVal; } }) } let app = new Observer({ name: 'liujianhuan', company: 'Qihoo 360', address: 'Chaoyang, Beijing' })
測試結果如下圖:
如上圖結果所示,上述代碼完美的實現了問題一中所提到的監聽對象屬性變化,但是深入思考就會發現,上述代碼還是有問題的,因此,引出來問題二。
問題二
如果傳入的參數對象是一個“比較深”的對象(也就是其屬性值也可能是對象),那該怎么辦?
舉個例子:
let app = new Observer({
basicInfo: {
name: 'liujianhuan', age: 25 }, company: 'Qihoo 360', address: 'Chaoyang, Beijing' }) //要實現的結果如下 app.data.basicInfo.name //你訪問了basicInfo,你訪問了name
首先利用問題一中的代碼進行測試:
從結果可以看到並不能解決問題,到這里也許有人覺得只要在代碼中加上這樣的一段代碼即可:
for(let key in data){ if(data.hasOwnProperty(key) && typeof data[key] === 'object'){ new Observer(data[key]); } }
事實上是這種方式是無效的,讀者可自行測試。究其原因,ES6中的proxy方式是通過Proxy構造函數來new一個實例,此實例代理攔截目標對象的操作,所以對於深層遞歸new出來的子對象實例我們是無法操作的,所以這種方法無效。
一步一步寫的現在是不是覺得人生好無趣,好不容易寫了這么多卻發現行不通啊。這時候先上一碗熱雞湯,人生的每一步都是我們應該走的,因為它會給我們不同的經歷,讓我們更堅韌、更強大。 此路不通,那就只能再回首走開篇提到的第二條路了,不過這時候也應該上一碗毒雞湯,所謂,碼農之路就是,山重水復疑無路,柳暗花明又一坑。
第二種方法是采用ES5中的defineProperty,為目標對象的屬性添加setter和getter。關於defineProperty的基本知識這里不再贅述,有不清楚的地方可以自行翻閱權威書籍,比如紅寶書。直接上代碼,下邊的代碼涵蓋了問題一和二。
function Observer (data) { //暫不考慮數組 this.data = data; this.makeObserver(data); } Observer.prototype.setterAndGetter = function (key, val) { //此為問題一的要點 Object.defineProperty(this.data, key, { enumerable: true, configurable: true, get: function(){ console.log('你訪問了' + key); return val; }, set: function(newVal){ console.log('你設置了' + key); console.log('新的' + key + '=' + newVal); val = newVal; } }) } Observer.prototype.makeObserver = function (obj) { let val; //此為問題二的要點 for(let key in obj){ if(obj.hasOwnProperty(key)){ val = obj[key]; //深度遍歷 if(typeof val === 'object'){ new Observer(val); } } this.setterAndGetter(key, val); } } //測試 let app = new Observer({ basicInfo: { name: 'liujianhuan', age: 25 }, company: 'Qihoo 360', address: 'Chaoyang, Beijing'
})
測試結果如下圖:
看到這樣的結果是不是很開心呢,同時解決了問題一和問題二,perfect。如果你這樣想了,那就得回想一下毒雞湯了,生活中不是缺少坑,是缺少發現坑的眼睛,直到被你柳暗花明之后踩到。。請繼續看問題三。
問題三
如果設置新的值是一個對象的話,新設置的對象的屬性是否能繼續響應getter和setter呢?
舉個例子:
let app = new Observer({
basicInfo: {
name: 'liujianhuan', age: 25 }, company: 'Qihoo 360', address: 'Chaoyang, Beijing' }) //要實現的結果如下 app.data.basicInfo = {like: 'NBA'}//你設置了basicInfo,新的basicInfo為{like: 'NBA'} app.data.basicInfo.like //你訪問了basicInfo,你訪問了like
采用問題二中的代碼進行測試:
看到了吧,如果設置新的值是一個對象的話,新設置的對象的屬性不能繼續響應getter和setter。不過代碼寫到這里,這個問題應該是非常容易的就可以解決了,那就是直接在setter中添加如下代碼:
//如果newval是對象的話 if(typeof newVal === 'object'){ new Observer(val); }
測試結果如下:
至此,我們已經較為完整的了實現了針對對象變化的數據監聽,由於數組的操作方法比較多,所以針對數組的變化監聽待后續完善,接下來我們針對上述代碼繼續增強完善。
完善點一
考慮傳遞回調函數。在實際應用中,當特定數據發生改變的時候,我們是希望做一些特定的事情,而不是每一次只能打印出來一些信息,所以,我們需要支持傳入回調函數的功能。
舉個例子:
let app = new Observer({ name: 'liujianhuan', age: 25, company: 'Qihoo 360', address: 'Chaoyang, Beijing' }) app.$watch('age', function(age){ console.log(`我的年齡變了,現在是:${age}歲了`); }) app.data.basicInfo.age = 20;//輸出:'我的年齡變了,現在已經是20歲了'
針對上述場景,我們需要實現$watch這個API,每當年齡發生改變的時候觸發相應的回調函數。這個API的實現可以很有多種方式,在此我們采用事件的方式來實現,通俗的講就是實現一個通用的事件模型,每次$watch一個屬性相當於注冊了一個監聽事件,當屬性發生改變的則觸發對應的事件,這樣做的優勢是可以為同一個屬性通過事件模型來注冊多個回調函數。
下邊是一個不完整的簡易事件模型:
//實現一個事件 function Event(){ this.events = {}; } Event.prototype.on = function(attr, callback){ if(this.events[attr]){ this.events[attr].push(callback); }else{ this.events[attr] = [callback]; } } Event.prototype.off = function(attr){ for(let key in this.events){ if(this.events.hasOwnProperty(key) && key === attr){ delete this.events[key]; } } } Event.prototype.emit = function(attr, ...arg){ this.events[attr] && this.events[attr].forEach(function(item){ item(...arg); }) }
有了上述事件模型后,每次new一個Observer的實例時,就new一個Event實例出來用來管理Observer實例中的所有事件;然后通過$watch API來為Observer實例注冊屬性的監聽事件,每次當屬性改變的觸發相應的事件隊列。
function Observer (data) { //暫不考慮數組 this.data = data; this.makeObserver(data); this.eventsBus = new Event(); } Observer.prototype.setterAndGetter = function (key, val) { let _this = this; Object.defineProperty(this.data, key, { enumerable: true, configurable: true, get: function(){ console.log('你訪問了' + key); return val; }, set: function(newVal){ console.log('你設置了' + key); console.log('新的' + key + '=' + newVal); //觸發$watch函數 _this.eventsBus.emit(key, val, newVal); val = newVal; //如果newval是對象的話 if(typeof newVal === 'object'){ new Observer(val); } } }) } Observer.prototype.makeObserver = function (obj) { let val; for(let key in obj){ if(obj.hasOwnProperty(key)){ val = obj[key]; //深度遍歷 if(typeof val === 'object'){ new Observer(val); } } this.setterAndGetter(key, val); } } Observer.prototype.$watch = function(attr, callback){ this.eventsBus.on(attr, callback); } let app = new Observer({ name: 'liujianhuan', age: 25, company: 'Qihoo 360', address: 'Chaoyang, Beijing' }) app.$watch('age', function(oldVal, newVal){ console.log(`我的年齡變了,原來是: ${oldVal}歲,現在是:${newVal}歲了`) }) app.$watch('age', function(oldVal, newVal){ console.log(`我的年齡真的變了誒,竟然年輕了${oldVal - newVal}歲`) }) app.data.basicInfo.age = 20;
測試結果如下:
測試結果顯示上述代碼觸發了所注冊的兩個回調,但是上述代碼也還是有着問題,比如目前只可注冊監聽對象的第一層的屬性,對於對象的深層屬性並不能有效監聽,比如:
let app = new Observer({ basicInfo: { name: 'liujianhuan', age: 25 }, company: 'Qihoo 360', address: 'Chaoyang, Beijing' }) app.$watch('age', function(age){ console.log(`我的年齡變了,現在是:${age}歲了`); }) app.data.basicInfo.age = 20;
這段代碼中的回調並不會觸發,這個問題留下來在后續中完善補充。
總結一下本文中針對“動態數據綁定”還未解決掉的問題:
- 當傳入的參數為數組時,如何監聽數組對象的變化
- 深層對象屬性的事件回調監聽,或者描述為:對象的深層屬性值發生變化后如何向上傳遞到頂層
- 動態數據與視圖的綁定,如何綁定,當數據變化后如何觸發視圖的自動刷新。
另外附上兩個最近使用Vue實現的 Vue在SKU組合查詢中應用 和 基於Vue的后台管理模板
---恢復內容結束---