緣起
前幾天在看一些流行的迷你mvvm框架(好比 avalon.js 、 vue.js 這種較輕的框架,而非Angularjs、Emberjs這種較重的框架)的實現。現代流行的mvvm框架通常都會將數據雙向綁定(two-ways data binding)作掉,做爲框架自身的一個賣點( Ember.js 貌似是不支持數據雙向綁定的。),並且每種框架雙向數據綁定的實現方式都不太一致,好比Anguarjs內部使用的是 髒檢查 ,而avalon.js內部實現方式的本質是設置 屬性訪問器 。javascript
這裏不打算具體的討論各個框架對雙向數據綁定的具體實現,僅說一下前端實現雙向數據綁定的幾種經常使用方法,並着重講一下avalon.js實現雙向數據綁定的技術選型。php
雙向數據綁定的常規實現方式
首先咱們來講一下何爲前端的 雙向數據綁定 。簡單的來講,就是框架的控制器層(這裏的控制器層是一個泛指,能夠理解爲控制view行爲和聯繫model層的中間件)和UI展現層(view層)創建一個雙向的數據通道。當這兩層中的任何一方發生變化時,另外一層將會當即(或者看起來是 當即 )自動做出相應的變化。html
通常來講要實現這種雙向數據綁定關係(控制器層與展現層的關聯過程),在前端目前會有三種方式,前端
- 髒檢查
- 觀察機制
- 封裝屬性訪問器
髒檢查
咱們說Angularjs(這裏特指AngularJS 1.x.x版本,不表明AngularJS 2.x.x版本)雙向數據綁定的技術實現是髒檢查,大體的原理就是,Angularjs內部會維護一個序列,將全部須要監控的屬性放在這個序列中,當發生某些特定事件時(注意,這裏並非定時的而是由某些特殊事件觸發的),Angularjs會調用 $digest
方法,這個方法內部作的邏輯就是遍歷全部的watcher,對被監控的屬性作對比,對比其在方法調用先後屬性值有沒有發生變化,若是發生變化,則調用對應的handler。網上有許多剖析Angularjs雙向數據綁定實現原理的文章,好比 這篇 ,再好比 這篇 ,等等。vue
這種方式的缺點很明顯,遍歷輪訓watcher是很是消耗性能的,特別是當單頁的監控數量達到一個數量級的時候。java
觀察機制
博主以前有一篇轉載翻譯的文章, Object.observe()帶來的數據綁定變革 ,說的就是使用ECMAScript7中的 Object.observe
方法對對象(或者其屬性)進行監控觀察,一旦其發生變化時,將會執行相應的handler。git
這是目前監控屬性數據變動最完美的一種方法,語言(瀏覽器)原生支持,沒有什麼比這個更好了。惟一的遺憾就是目前支持廣度還不行,有待全面推廣。github
封裝屬性訪問器
在php中有 魔術方法 這樣一種概念,好比php中的 __get()
和 __set()
方法。在javascript中也有相似的概念,不過不叫魔術方法,而是叫作訪問器。咱們來看個示例代碼,瀏覽器
var data ={ name:"erik", getName:function(){ returnthis.name; }, setName:function(name){ this.name = name; } };
從上面的代碼中咱們能夠管中窺豹,好比 data
中的 getName()
和 setName()
方法,咱們能夠簡單的將其當作 data.name
的訪問器(或者叫作 存取器 )。框架
其實,針對上述的代碼,更加嚴格一點的話,不容許直接訪問 data.name
屬性,全部對 data.name
的讀寫都必須經過data.getName()
和 data.setName()
方法。因此,想象一下,一旦某個屬性不容許對其進行直接讀寫,而必須是經過訪問器進行讀寫時,那麼我固然經過重寫屬性的訪問器方法來作一些額外的情,好比屬性值變動監控。使用屬性訪問器來作數據雙向綁定的原理就是在此。
這種方法固然也有弊端,最突出的就是每添加一個屬性監控,都必須爲這個屬性添加對應訪問器方法,不然這個屬性的變動就沒法捕獲。
Object.defineProperty
方法
國產mvvm框架avalon.js實現數據雙向綁定的原理就是屬性訪問器。不過它固然不會像上述示例代碼同樣原始。它使用了ECMAScript5.1(ECMA-262)中定義的標準屬性 Object.defineProperty
方法。針對國內行情,部分還不支持Object.defineProperty
低級瀏覽器採用VBScript做了完美兼容,不像其餘的mvvm框架已經逐漸放棄對低端瀏覽器的支持。
咱們先來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
,待修改屬性的相關描述
descriptor
要求傳入一個對象,其默認值以下,
/** * @{param} descriptor */ { configurable:false, enumerable:false, writable:false, value:null, set:undefined, get:undefined }
-
configurable
,屬性是否可配置。可配置的含義包括:是否能夠刪除屬性(delete
),是否能夠修改屬性的writable
、enumerable
、configurable
屬性。 -
enumerable
,屬性是否可枚舉。可枚舉的含義包括:是否能夠經過for...in
遍歷到,是否能夠經過Object.keys()
方法獲取屬性名稱。 -
writable
,屬性是否可重寫。可重寫的含義包括:是否能夠對屬性進行從新賦值。 -
value
,屬性的默認值。 -
set
,屬性的重寫器(暫且這麼叫)。一旦屬性被從新賦值,此方法被自動調用。 -
get
,屬性的讀取器(暫且這麼叫)。一旦屬性被訪問讀取,此方法被自動調用。
下面來一段示例代碼,
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, 屬性刪除的動做也是無效的
通過上述的示例,正常狀況下 Object.definePropert()
的使用都是比較簡單的。
不過仍是有一點須要額外注意一下, Object.defineProperty()
方法設置屬性時,屬性不能同時聲明訪問器屬性( set
和get
)和 writable
或者 value
屬性。 意思就是,某個屬性設置了 writable
或者 value
屬性,那麼這個屬性就不能聲明get
和 set
了,反之亦然。
由於 Object.defineProperty()
在聲明一個屬性時,不容許同一個屬性出現兩種以上存取訪問控制。
示例代碼,
var o ={}, myName ='erik'; Object.defineProperty(o,'name',{ value: myName, set:function(name){ myName = name; }, get:function(){ return myName; } });
上面的代碼看起來貌似是沒有什麼問題,可是真正執行時會報錯,報錯以下,
TypeError:Invalidproperty. A property cannot both have accessors and be writable or have a value,#<Object>
由於這裏的 name
屬性同時聲明瞭 value
特性和 set
及 get
特性,這二者提供了兩種對 name
屬性的讀寫控制。這裏若是不聲明 value
特性,而是聲明 writable
特性,結果也是同樣的,一樣會報錯