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