MVVM雙向綁定實現之Object.defineProperty

  隨着web應用的發展,直接操做dom的應用已漸行漸遠,取而代之的是時下愈來愈流行的MVVM框架,dom操做幾乎絕跡,這裏面天然是框架底層封裝的結果。MVVM框架的雙向數據綁定使開發效率大大提升;而後在實現這些雙向數據綁定時,使用ES7原生的Object.observe方法則是完美解決方案,可是遺憾的是該方法目前仍是ES7的草案階段,各瀏覽器還不支持,目前chrome 36+支持該方法。前端

  既然Object.observe不被支持,可是其替代方案是ECMAScript 262v5帶來的新東西Object.defineProperty和Object.defineProperties方法也能夠很優雅的實現雙向數據綁定。主要是利用在定義一個對象的屬性時,能夠設置其set和get方法,用於對對象的該屬性進行設置或者讀取時的「攔截」操做,相似於"鉤子",即在必定的狀況下執行特定的邏輯;目前使用該方案實現的MVVM框架有avalon、vue等,可是目前比較流行的angularjs框架缺使用爲人詬病的"髒檢查"機制實現,這也是其性能遭人吐槽低的一個緣由,聽說目前angularjs2會改造這一實現機制。vue

好了,言歸正傳,接下來看看defineProperty的使用angularjs

一、用法

語法: Object.defineProperty(obj, attr, descriptor) web

從語法能夠看出,該方法接受三個參數:chrome

  obj 爲屬性attr所屬的對象;segmentfault

  attr 爲obj對象新定義或者修改的屬性名;數組

   descriptor 爲該對象屬性的描述符,其中其有6個配置項:瀏覽器

  • value: 屬性的值,默認undefined
  • configurable: 默認爲false,true表示當前屬性是否能夠被改變或者刪除,其中」改變「是指屬性的descriptor的配置項configurable、enumerable和writable的修改
  • enumerable:默認爲false,true表示當前屬性可否被for...in或者Objectk.keys語句枚舉
  • writable:默認爲false,true表示當前屬性的值能夠被賦值重寫
  • get:默認undefined,獲取目標屬性時執行的回調方法,該函數的返回值做爲該屬性的值
  • set:默認undefined,目標屬性的值被重寫時執行的回調

從上面的用法能夠知道:能夠經過設置get和set方法對屬性的讀取和修改進行攔截,經過將實現數據和視圖同步的邏輯置於這兩個方法中,從而實現數據變動視圖也能夠跟着同步,反之則須要配合事件了;前端框架

var obj = {};

Object.defineProperty(obj, 'name', {
    "value": "wonyun",
    "configurable": false
});

Object.getOwnPropertyDescriptor(obj, 'name') //Object {value: "wonyun", writable: false, enumerable: false, configurable: false}

 

二、注意事項

  上面介紹了Object.defineProperty的用法,可是其使用仍是有不少注意的地方,一不留心就會出錯。框架

  • 不容許同一個屬性存在兩個及以上的存取訪問器配置,因此writable或者value不能與get/set同時配置,不然報錯
    var obj = {};
    
    Object.defineProperty(obj, 'name', {
        "writable": false,
        "get": function(){},
        "set": function(){}
    });

    執行上面代碼,會發現控制檯報錯」Uncaught TypeError: Invalid property.  A property cannot both have accessors and be writable or have a value「,



  • configurable和writable設置爲false的區別是:前者不能在對目標屬性的configurable、enumerable和writable進行修改,修改也是無效;
    後者不能對目標屬性重寫賦值,賦值也無效;



  • 在使用Obejct.defineProperty方法定義對象屬性時,如果屬性描述符沒有配置的,configurable、enumerable和writable默認爲false,value默認爲undefined,而get/set則不會配置;而在使用非Object.defineProperty方法定義對象屬性,其configurable、enumerable和writable均爲true,value爲賦的值,get/setb不會配置;


三、兼容性

  就如本文開頭說的,Obejct.defineProperty方法ECMAScript 262v5帶來的新特性,支持ES5的瀏覽器纔可以使用該方法;固然這就涉及到其使用的瀏覽器兼容性問題;目前非IE瀏覽器的標準瀏覽器都支持該特性了,固然在天朝,你懂得還有些很多ie6-8的用戶,這些低版本的IE瀏覽器是不支持Object.defineProperty的。話說回來,隨着微軟本身放棄維護這些低版本的ie瀏覽器,ie6-8的使用佔比是愈來愈低,這也是當前一些前端框架不在支持ie六、七、8瀏覽器的一大緣由,像angularjs、vuejs都不支持低版本ie瀏覽器,看來低版本的ie6也快走到盡頭。

Feature Firefox (Gecko) Chrome Internet Explorer Opera Safari
Basic support 4.0 5 11.60 5.1

 

四、Object.defineProperties(obj, descriptors)

  Object.defineProperty方法只能定義單個對象的屬性,固然也就有批量定義對象屬性的方法,Object.defineProperties方法就是幹這個的:在一個對象添加或者修改一個或者多個屬性。它與Object.defineProperty不一樣的地方是它只有兩個參數,其中第二參數爲一個配置對象,其鍵值爲對象的屬性名,其值爲屬性名的配置項,如:

var obj = {}

Object.defineProperties(obj, {
  name: {
        value: "wonyun",
        writable: true,
        enumerable: true,
        configurable: true
    },
  age: {
        value: 20,
        writable: true
    }
})

 

五、Object.defineProperty在avalon中的運用

  avalon中的利用Object.defineProperty,爲其定義一個accessor來同步視圖,每次改變或者讀取vmodel對象中的屬性時候,會調用accessor方法用於同步視圖和其餘操做。

  1 function modelFactory($scope, $special, $model) {
  2         
  3       //省略初始化代碼
  4        ...
  5        ...
  6         var $vmodel = {} //要返回的對象, 它在IE6-8下可能被偷龍轉鳳
  7         $model = $model || {} //vmodels.$model屬性
  8         var $events = {} //vmodel.$events屬性
  9         var watchedProperties = {} //監控屬性
 10         var computedProperties = [] //計算屬性
 11         for (var i in $scope) {
 12             (function(name, val) {
 13                 $model[name] = val
 14                 ... //省略其餘無關代碼
 15 
 16                 //總共產生三種accessor
 17                 var accessor
 18                 var valueType = avalon.type(val)
 19                 $events[name] = []
 20                 //總共產生三種accessor
 21                 if (valueType === "object" && isFunction(val.get) && Object.keys(val).length <= 2) {
 22                     var setter = val.set
 23                     var getter = val.get
 24                     //第1種對應計算屬性, 因變量,經過其餘監控屬性觸發其改變
 25                     accessor = function(newValue) {
 26                         var $events = $vmodel.$events
 27                         var oldValue = $model[name]
 28                         if (arguments.length) {
 29                             if (stopRepeatAssign) {
 30                                 return
 31                             }
 32                             if (isFunction(setter)) {
 33                                 var backup = $events[name]
 34                                 $events[name] = [] //清空回調,防止內部冒泡而觸發屢次$fire
 35                                 setter.call($vmodel, newValue)
 36                                 $events[name] = backup
 37                             }
 38                         } else {
 39                             if (avalon.openComputedCollect) { // 收集視圖刷新函數
 40                                 collectSubscribers($events[name])
 41                             }
 42                         }
 43                         newValue = $model[name] = getter.call($vmodel) //同步$model
 44                         if (!isEqual(oldValue, newValue)) {
 45                             withProxyCount && updateWithProxy($vmodel.$id, name, newValue) //同步循環綁定中的代理VM
 46                             notifySubscribers($events[name]) //同步視圖
 47                             safeFire($vmodel, name, newValue, oldValue) //觸發$watch回調
 48                         }
 49                         return newValue
 50                     }
 51                     
 52                 .... // 省略無關代碼
 53     
 54                 } else if (rcomplexType.test(valueType)) {
 55                     //第2種對應子ViewModel或監控數組 
 56                     accessor = function(newValue) {
 57                         var childVmodel = accessor.child
 58                         var oldValue = $model[name]
 59                         if (arguments.length) {
 60                             if (stopRepeatAssign) {
 61                                 return
 62                             }
 63                             if (!isEqual(oldValue, newValue)) {
 64                                 childVmodel = accessor.child = updateChild($vmodel, name, newValue, valueType)
 65                                 newValue = $model[name] = childVmodel.$model //同步$model
 66                                 var fn = rebindings[childVmodel.$id]
 67                                 fn && fn() //同步視圖
 68                                 safeFire($vmodel, name, newValue, oldValue) //觸發$watch回調
 69                             }
 70                         } else {
 71                             collectSubscribers($events[name]) //收集視圖函數
 72                             return childVmodel
 73                         }
 74                     }
 75                     var childVmodel = accessor.child = modelFactory(val, 0, $model[name])
 76                     childVmodel.$events[subscribers] = $events[name]
 77                 } else {
 78                     //第3種對應簡單的數據類型,自變量,監控屬性
 79                     accessor = function(newValue) {
 80                         var oldValue = $model[name]
 81                         if (arguments.length) {
 82                             if (!isEqual(oldValue, newValue)) {
 83                                 $model[name] = newValue //同步$model
 84                                 withProxyCount && updateWithProxy($vmodel.$id, name, newValue) //同步代理VM
 85                                 notifySubscribers($events[name]) //同步視圖
 86                                 safeFire($vmodel, name, newValue, oldValue) //觸發$watch回調
 87                             }
 88                         } else {
 89                             collectSubscribers($events[name])
 90                             return oldValue
 91                         }
 92                     }
 93                 }
 94                 watchedProperties[name] = accessor
 95             })(i, $scope[i])
 96         }
 97 
 98         ... //省略無關代碼
 99 
100         $vmodel = defineProperties($vmodel, descriptorFactory(watchedProperties), $scope) //生成一個空的ViewModel
101         
102        ... //省略無關代碼
103         return $vmodel
104     } 
105 
106 //生成avalon的vmodel的Object.defineProperty方法
107 
108 var descriptorFactory = W3C ? function(obj) {
109         var descriptors = {}
110         for (var i in obj) {
111             descriptors[i] = {
112                 get: obj[i],
113                 set: obj[i],
114                 enumerable: true,
115                 configurable: true
116             }
117         }
118         return descriptors
119     } : function(a) {
120         return a
121     }
View Code

 

參考文獻:

相關文章
相關標籤/搜索