前幾天在看一些流行的迷你mvvm框架(比如avalon.js、vue.js這種較輕的框架,而非Angularjs、Emberjs這種較重的框架)的實現(xiàn)。現(xiàn)代流行的mvvm框架一般都會將數(shù)據(jù)雙向綁定(two-ways data binding)做掉,作為框架自身的一個賣點(Ember.js貌似是不支持?jǐn)?shù)據(jù)雙向綁定的。),而且每種框架雙向數(shù)據(jù)綁定的實現(xiàn)方式都不太一致,比如Anguarjs內(nèi)部使用的是臟檢查,而avalon.js內(nèi)部實現(xiàn)方式的本質(zhì)是設(shè)置屬性訪問器。
這里不打算具體的討論各個框架對雙向數(shù)據(jù)綁定的具體實現(xiàn),僅說一下前端實現(xiàn)雙向數(shù)據(jù)綁定的幾種常用方法,并著重講一下avalon.js實現(xiàn)雙向數(shù)據(jù)綁定的技術(shù)選型。
首先我們來說一下何為前端的雙向數(shù)據(jù)綁定。簡單的來說,就是框架的控制器層(這里的控制器層是一個泛指,可以理解為控制view行為和聯(lián)系model層的中間件)和UI展示層(view層)建立一個雙向的數(shù)據(jù)通道。當(dāng)這兩層中的任何一方發(fā)生變化時,另一層將會立即(或者看起來是立即)自動作出相應(yīng)的變化。
一般來說要實現(xiàn)這種雙向數(shù)據(jù)綁定關(guān)系(控制器層與展示層的關(guān)聯(lián)過程),在前端目前會有三種方式,
我們說Angularjs(這里特指AngularJS 1.x.x版本,不代表AngularJS 2.x.x版本)雙向數(shù)據(jù)綁定的技術(shù)實現(xiàn)是臟檢查,大致的原理就是,Angularjs內(nèi)部會維護(hù)一個序列,將所有需要監(jiān)控的屬性放在這個序列中,當(dāng)發(fā)生某些特定事件時(注意,這里并不是定時的而是由某些特殊事件觸發(fā)的),Angularjs會調(diào)用$digest
方法,這個方法內(nèi)部做的邏輯就是遍歷所有的watcher,對被監(jiān)控的屬性做對比,對比其在方法調(diào)用前后屬性值有沒有發(fā)生變化,如果發(fā)生變化,則調(diào)用對應(yīng)的handler。網(wǎng)上有許多剖析Angularjs雙向數(shù)據(jù)綁定實現(xiàn)原理的文章,比如這篇,再比如這篇,等等。
這種方式的缺點很明顯,遍歷輪訓(xùn)watcher是非常消耗性能的,特別是當(dāng)單頁的監(jiān)控數(shù)量達(dá)到一個數(shù)量級的時候。
博主之前有一篇轉(zhuǎn)載翻譯的文章,Object.observe()帶來的數(shù)據(jù)綁定變革,說的就是使用ECMAScript7中的Object.observe
方法對對象(或者其屬性)進(jìn)行監(jiān)控觀察,一旦其發(fā)生變化時,將會執(zhí)行相應(yīng)的handler。
這是目前監(jiān)控屬性數(shù)據(jù)變更最完美的一種方法,語言(瀏覽器)原生支持,沒有什么比這個更好了。唯一的遺憾就是目前支持廣度還不行,有待全面推廣。
在php中有魔術(shù)方法這樣一種概念,比如php中的__get()
和__set()
方法。在javascript中也有類似的概念,不過不叫魔術(shù)方法,而是叫做訪問器。我們來看個示例代碼,
var data = {
name: "erik",
getName: function() {
return this.name;
},
setName: function(name) {
this.name = name;
}
};
從上面的代碼中我們可以管中窺豹,比如data
中的getName()
和setName()
方法,我們可以簡單的將其看成data.name
的訪問器(或者叫做存取器)。
其實,針對上述的代碼,更加嚴(yán)格一點的話,不允許直接訪問data.name
屬性,所有對data.name
的讀寫都必須通過data.getName()
和data.setName()
方法。所以,想象一下,一旦某個屬性不允許對其進(jìn)行直接讀寫,而必須是通過訪問器進(jìn)行讀寫時,那么我當(dāng)然通過重寫屬性的訪問器方法來做一些額外的情,比如屬性值變更監(jiān)控。使用屬性訪問器來做數(shù)據(jù)雙向綁定的原理就是在此。
這種方法當(dāng)然也有弊端,最突出的就是每添加一個屬性監(jiān)控,都必須為這個屬性添加對應(yīng)訪問器方法,否則這個屬性的變更就無法捕獲。
Object.defineProperty
方法國產(chǎn)mvvm框架avalon.js實現(xiàn)數(shù)據(jù)雙向綁定的原理就是屬性訪問器。不過它當(dāng)然不會像上述示例代碼一樣原始。它使用了ECMAScript5.1(ECMA-262)中定義的標(biāo)準(zhǔn)屬性Object.defineProperty
方法。針對國內(nèi)行情,部分還不支持Object.defineProperty
低級瀏覽器采用VBScript作了完美兼容,不像其他的mvvm框架已經(jīng)逐漸放棄對低端瀏覽器的支持。
我們先來MDN上對Object.defineProperty
方法的定義,
The Object.defineProperty() method defines a new property directly on an object, or modifies an existing property on an object, and returns the object.
意義很明確,Object.defineProperty
方法提供了一種直接的方式來定義對象屬性或者修改已有對象屬性。其方法原型如下,
Object.defineProperty(obj, prop, descriptor)
其中,
obj
,待修改的對象prop
,帶修改的屬性名稱descriptor
,待修改屬性的相關(guān)描述descriptor
要求傳入一個對象,其默認(rèn)值如下,
/**
* @{param} descriptor
*/
{
configurable: false,
enumerable: false,
writable: false,
value: null,
set: undefined,
get: undefined
}
configurable
,屬性是否可配置??膳渲玫暮x包括:是否可以刪除屬性(delete
),是否可以修改屬性的writable
、enumerable
、configurable
屬性。enumerable
,屬性是否可枚舉??擅杜e的含義包括:是否可以通過for...in
遍歷到,是否可以通過Object.keys()
方法獲取屬性名稱。writable
,屬性是否可重寫??芍貙懙暮x包括:是否可以對屬性進(jìn)行重新賦值。value
,屬性的默認(rèn)值。set
,屬性的重寫器(暫且這么叫)。一旦屬性被重新賦值,此方法被自動調(diào)用。get
,屬性的讀取器(暫且這么叫)。一旦屬性被訪問讀取,此方法被自動調(diào)用。下面來一段示例代碼,
var o = {};
Object.defineProperty(o, 'name', {
value: 'erik'
});
console.log(Object.getOwnPropertyDescriptor(o, 'name')); // Object {value: "erik", writable: false, enumerable: false, configurable: false}
Object.defineProperty(o, 'age', {
value: 26,
configurable: true,
writable: true
});
console.log(o.age); // 26
o.age = 18;
console.log(o.age); // 18. 因為age屬性是可重寫的
console.log(Object.keys(o)); // []. name和age屬性都不是可枚舉的
Object.defineProperty(o, 'sex', {
value: 'male',
writable: false
});
o.sex = 'female'; // 這里的賦值其實是不起作用的
console.log(o.sex); // 'male';
delete o.sex; // false, 屬性刪除的動作也是無效的
經(jīng)過上述的示例,正常情況下Object.definePropert()
的使用都是比較簡單的。
不過還是有一點需要額外注意一下,Object.defineProperty()
方法設(shè)置屬性時,屬性不能同時聲明訪問器屬性(set
和get
)和writable
或者value
屬性。意思就是,某個屬性設(shè)置了writable
或者value
屬性,那么這個屬性就不能聲明get
和set
了,反之亦然。
因為Object.defineProperty()
在聲明一個屬性時,不允許同一個屬性出現(xiàn)兩種以上存取訪問控制。
示例代碼,
var o = {},
myName = 'erik';
Object.defineProperty(o, 'name', {
value: myName,
set: function(name) {
myName = name;
},
get: function() {
return myName;
}
});
上面的代碼看起來貌似是沒有什么問題,但是真正執(zhí)行時會報錯,報錯如下,
TypeError: Invalid property. A property cannot both have accessors and be writable or have a value, #<Object>
因為這里的name
屬性同時聲明了value
特性和set
及get
特性,這兩者提供了兩種對name
屬性的讀寫控制。這里如果不聲明value
特性,而是聲明writable
特性,結(jié)果也是一樣的,同樣會報錯。
更多建議: