JS中的雙向資料繫結及Object.defineProperty方法
緣起
前幾天在看一些流行的迷你mvvm框架(比如avalon.js、vue.js 這種較輕的框架,而非Angularjs、Emberjs這種較重的框架)的實現。現代流行的mvvm框架一般都會將資料雙向繫結(two-ways data binding)做掉,作為框架自身的一個賣點(Ember.js 貌似是不支援資料雙向繫結的。),而且每種框架雙向資料繫結的實現方式都不太一致,比如Anguarjs內部使用的是髒檢查 ,而avalon.js內部實現方式的本質是設定屬性訪問器 。
這裡不打算具體的討論各個框架對雙向資料繫結的具體實現,僅說一下前端實現雙向資料繫結的幾種常用方法,並著重講一下avalon.js實現雙向資料繫結的技術選型。
雙向資料繫結的常規實現方式
首先我們來說一下何為前端的雙向資料繫結 。簡單的來說,就是框架的控制器層(這裡的控制器層是一個泛指,可以理解為控制view行為和聯絡model層的中介軟體)和UI展示層(view層)建立一個雙向的資料通道。當這兩層中的任何一方發生變化時,另一層將會立即(或者看起來是立即 )自動作出相應的變化。
一般來說要實現這種雙向資料繫結關係(控制器層與展示層的關聯過程),在前端目前會有三種方式,
- 髒檢查
- 觀察機制
- 封裝屬性訪問器
髒檢查
我們說Angularjs(這裡特指AngularJS 1.x.x版本,不代表AngularJS 2.x.x版本)雙向資料繫結的技術實現是髒檢查,大致的原理就是,Angularjs內部會維護一個序列,將所有需要監控的屬性放在這個序列中,當發生某些特定事件時(注意,這裡並不是定時的而是由某些特殊事件觸發的),Angularjs會呼叫$digest
方法,這個方法內部做的邏輯就是遍歷所有的watcher,對被監控的屬性做對比,對比其在方法呼叫前後屬性值有沒有發生變化,如果發生變化,則呼叫對應的handler。網上有許多剖析Angularjs雙向資料繫結實現原理的文章,比如這篇 ,再比如這篇 ,等等。
這種方式的缺點很明顯,遍歷輪訓watcher是非常消耗效能的,特別是當單頁的監控數量達到一個數量級的時候。
觀察機制
博主之前有一篇轉載翻譯的文章,ofollow,noindex">Object.observe()帶來的資料繫結變革
,說的就是使用ECMAScript7中的JavaScript%2FReference%2FGlobal_Objects%2FObject%2Fobserve" target="_blank" rel="nofollow,noindex">
Object.observe
方法對物件(或者其屬性)進行監控觀察,一旦其發生變化時,將會執行相應的handler。
這是目前監控屬性資料變更最完美的一種方法,語言(瀏覽器)原生支援,沒有什麼比這個更好了。唯一的遺憾就是目前支援廣度還不行,有待全面推廣。
封裝屬性訪問器
在php中有魔術方法 這樣一種概念,比如php中的__get()
和__set()
方法。在javascript中也有類似的概念,不過不叫魔術方法,而是叫做訪問器。我們來看個示例程式碼,
var data = { name: "erik", getName: function() { return this.name; }, setName: function(name) { this.name = name; } }; </pre> 複製程式碼
從上面的程式碼中我們可以管中窺豹,比如data
中的getName()
和setName()
方法,我們可以簡單的將其看成data.name
的訪問器(或者叫做存取器
)。
其實,針對上述的程式碼,更加嚴格一點的話,不允許直接訪問data.name
屬性,所有對data.name
的讀寫都必須通過data.getName()
和data.setName()
方法。所以,想象一下,一旦某個屬性不允許對其進行直接讀寫,而必須是通過訪問器進行讀寫時,那麼我當然通過重寫屬性的訪問器方法來做一些額外的情,比如屬性值變更監控。使用屬性訪問器來做資料雙向繫結的原理就是在此。
這種方法當然也有弊端,最突出的就是每新增一個屬性監控,都必須為這個屬性新增對應訪問器方法,否則這個屬性的變更就無法捕獲。
Object.defineProperty 方法
國產mvvm框架avalon.js實現資料雙向繫結的原理就是屬性訪問器。不過它當然不會像上述示例程式碼一樣原始。它使用了ECMAScript5.1(ECMA-262)中定義的標準屬性JavaScript%2FReference%2FGlobal_Objects%2FObject%2FdefineProperty" target="_blank" rel="nofollow,noindex">
Object.defineProperty
方法。針對國內行情,部分還不支援Object.defineProperty
低階瀏覽器採用VBScript作了完美相容,不像其他的mvvm框架已經逐漸放棄對低端瀏覽器的支援。
我們先來MDN上對Object.defineProperty
方法的定義,
TheObject.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) </pre> </figure> 複製程式碼
其中,
obj prop descriptor
descriptor
要求傳入一個物件,其預設值如下,
* @{param} descriptor */ }//歡迎加入全棧開發交流圈一起學習交流:582735936 ]//面向1-3年前端人員 }//幫助突破技術瓶頸,提升思維能力 { configurable: false, enumerable: false, writable: false, value: null, set: undefined, get: undefined } </pre> </figure> 複製程式碼
-
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 }); }//歡迎加入全棧開發交流圈一起學習交流:582735936 ]//面向1-3年前端人員 }//幫助突破技術瓶頸,提升思維能力 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, 屬性刪除的動作也是無效的 </pre> </figure> 複製程式碼
經過上述的示例,正常情況下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; } }); </pre> </figure> 複製程式碼
上面的程式碼看起來貌似是沒有什麼問題,但是真正執行時會報錯,報錯如下,
TypeError: Invalid property.A property cannot both have accessors and be writable or have a value, #<Object> </pre> </figure> 複製程式碼
因為這裡的name
屬性同時聲明瞭value
特性和set
及get
特性,這兩者提供了兩種對name
屬性的讀寫控制。這裡如果不宣告value
特性,而是宣告writable
特性,結果也是一樣的,同樣會報錯。