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層)建立一個雙向的數據通道。當這兩層中的任何一方發生變化時,另一層將會立即(或者看起來是 立即

)自動作出相應的變化。

一般來說要實現這種雙向數據綁定關係(控制器層與展示層的關聯過程),在前端目前會有三種方式,

  1. 髒檢查
  2. 觀察機制
  3. 封裝屬性訪問器

髒檢查

我們說Angularjs(這裡特指AngularJS 1.x.x版本,不代表AngularJS 2.x.x版本)雙向數據綁定的技術實現是髒檢查,大致的原理就是,Angularjs內部會維護一個序列,將所有需要監控的屬性放在這個序列中,當發生某些特定事件時(注意,這裡並不是定時的而是由某些特殊事件觸發的),Angularjs會調用 $digest 方法,這個方法內部做的邏輯就是遍歷所有的watcher,對被監控的屬性做對比,對比其在方法調用前後屬性值有沒有發生變化,如果發生變化,則調用對應的handler。網上有許多剖析Angularjs雙向數據綁定實現原理的文章,比如 這篇 ,再比如 這篇 ,等等。

這種方式的缺點很明顯,遍歷輪訓watcher是非常消耗性能的,特別是當單頁的監控數量達到一個數量級的時候。

觀察機制

博主之前有一篇轉載翻譯的文章, Object.observe()帶來的數據綁定變革 ,說的就是使用ECMAScript7中的 Object.observe 方法對對象(或者其屬性)進行監控觀察,一旦其發生變化時,將會執行相應的handler。

這是目前監控屬性數據變更最完美的一種方法,語言(瀏覽器)原生支持,沒有什麼比這個更好了。唯一的遺憾就是目前支持廣度還不行,有待全面推廣。

封裝屬性訪問器

在php中有 魔術方法 這樣一種概念,比如php中的 __get() 和 __set() 方法。在javascript中也有類似的概念,不過不叫魔術方法,而是叫做訪問器。我們來看個示例代碼,

var data = {
name: "erik",
getName: function() {
return this.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
*/
  }//歡迎加入全棧開發交流圈一起學習交流:582735936
  ]//面向1-3年前端人員
  } //幫助突破技術瓶頸,提升思維能力
{
configurable: false,
enumerable: false,
writable: false,
value: null,
set: undefined,
get: undefined
}


  1. configurable ,屬性是否可配置。可配置的含義包括:是否可以刪除屬性( delete),是否可以修改屬性的 writable 、 enumerable 、 configurable 屬性。
  2. enumerable ,屬性是否可枚舉。可枚舉的含義包括:是否可以通過 for...in 遍歷到,是否可以通過 Object.keys() 方法獲取屬性名稱。
  3. writable ,屬性是否可重寫。可重寫的含義包括:是否可以對屬性進行重新賦值。
  4. value ,屬性的默認值。
  5. set ,屬性的重寫器(暫且這麼叫)。一旦屬性被重新賦值,此方法被自動調用。
  6. 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, 屬性刪除的動作也是無效的


經過上述的示例,正常情況下 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: Invalid property. A property cannot both have accessors and be writable or have a value, #


因為這裡的 name 屬性同時聲明瞭 value 特性和 set 及 get 特性,這兩者提供了兩種對 name 屬性的讀寫控制。這裡如果不聲明 value 特性,而是聲明 writable 特性,結果也是一樣的,同樣會報錯。


分享到:


相關文章: