JS中的雙向數(shù)據(jù)綁定及Object.defineProperty方法

2018-06-09 18:00 更新

緣起

前幾天在看一些流行的迷你mvvm框架(比如avalon.js、vue.js這種較輕的框架,而非Angularjs、Emberjs這種較重的框架)的實現(xiàn)。現(xiàn)代流行的mvvm框架一般都會將數(shù)據(jù)雙向綁定(two-ways data binding)做掉,作為框架自身的一個賣點(Ember.js貌似是不支持?jǐn)?shù)據(jù)雙向綁定的。),而且每種框架雙向數(shù)據(jù)綁定的實現(xiàn)方式都不太一致,比如Anguarjs內(nèi)部使用的是臟檢查,而avalon.js內(nèi)部實現(xiàn)方式的本質(zhì)是設(shè)置屬性訪問器

這里不打算具體的討論各個框架對雙向數(shù)據(jù)綁定的具體實現(xiàn),僅說一下前端實現(xiàn)雙向數(shù)據(jù)綁定的幾種常用方法,并著重講一下avalon.js實現(xiàn)雙向數(shù)據(jù)綁定的技術(shù)選型。

雙向數(shù)據(jù)綁定的常規(guī)實現(xiàn)方式

首先我們來說一下何為前端的雙向數(shù)據(jù)綁定。簡單的來說,就是框架的控制器層(這里的控制器層是一個泛指,可以理解為控制view行為和聯(lián)系model層的中間件)和UI展示層(view層)建立一個雙向的數(shù)據(jù)通道。當(dāng)這兩層中的任何一方發(fā)生變化時,另一層將會立即(或者看起來是立即)自動作出相應(yīng)的變化。

一般來說要實現(xiàn)這種雙向數(shù)據(jù)綁定關(guān)系(控制器層與展示層的關(guān)聯(lián)過程),在前端目前會有三種方式,

  1. 臟檢查
  2. 觀察機(jī)制
  3. 封裝屬性訪問器

臟檢查

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

這種方式的缺點很明顯,遍歷輪訓(xùn)watcher是非常消耗性能的,特別是當(dāng)單頁的監(jiān)控數(shù)量達(dá)到一個數(shù)量級的時候。

觀察機(jī)制

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

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

封裝屬性訪問器

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

var data = {
    name: "erik",
    getName: function() {
        return this.name;
    },
    setName: function(name) {
        this.name = name;
    }
};

從上面的代碼中我們可以管中窺豹,比如data中的getName()setName()方法,我們可以簡單的將其看成data.name的訪問器(或者叫做存取器)。

其實,針對上述的代碼,更加嚴(yán)格一點的話,不允許直接訪問data.name屬性,所有對data.name的讀寫都必須通過data.getName()data.setName()方法。所以,想象一下,一旦某個屬性不允許對其進(jìn)行直接讀寫,而必須是通過訪問器進(jìn)行讀寫時,那么我當(dāng)然通過重寫屬性的訪問器方法來做一些額外的情,比如屬性值變更監(jiān)控。使用屬性訪問器來做數(shù)據(jù)雙向綁定的原理就是在此。

這種方法當(dāng)然也有弊端,最突出的就是每添加一個屬性監(jiān)控,都必須為這個屬性添加對應(yīng)訪問器方法,否則這個屬性的變更就無法捕獲。

Object.defineProperty方法

國產(chǎn)mvvm框架avalon.js實現(xiàn)數(shù)據(jù)雙向綁定的原理就是屬性訪問器。不過它當(dāng)然不會像上述示例代碼一樣原始。它使用了ECMAScript5.1(ECMA-262)中定義的標(biāo)準(zhǔn)屬性Object.defineProperty方法。針對國內(nèi)行情,部分還不支持Object.defineProperty低級瀏覽器采用VBScript作了完美兼容,不像其他的mvvm框架已經(jīng)逐漸放棄對低端瀏覽器的支持。

我們先來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,待修改屬性的相關(guān)描述

descriptor要求傳入一個對象,其默認(rèn)值如下,

/**
 * @{param} descriptor
 */
{
    configurable: false,
    enumerable: false,
    writable: false,
    value: null,
    set: undefined,
    get: undefined
}
  1. configurable,屬性是否可配置??膳渲玫暮x包括:是否可以刪除屬性(delete),是否可以修改屬性的writableenumerable、configurable屬性。
  2. enumerable,屬性是否可枚舉??擅杜e的含義包括:是否可以通過for...in遍歷到,是否可以通過Object.keys()方法獲取屬性名稱。
  3. writable,屬性是否可重寫??芍貙懙暮x包括:是否可以對屬性進(jìn)行重新賦值。
  4. value,屬性的默認(rèn)值。
  5. set,屬性的重寫器(暫且這么叫)。一旦屬性被重新賦值,此方法被自動調(diào)用。
  6. get,屬性的讀取器(暫且這么叫)。一旦屬性被訪問讀取,此方法被自動調(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. 因為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, 屬性刪除的動作也是無效的

經(jīng)過上述的示例,正常情況下Object.definePropert()的使用都是比較簡單的。

不過還是有一點需要額外注意一下,Object.defineProperty()方法設(shè)置屬性時,屬性不能同時聲明訪問器屬性(setget)和writable或者value屬性。意思就是,某個屬性設(shè)置了writable或者value屬性,那么這個屬性就不能聲明getset了,反之亦然。

因為Object.defineProperty()在聲明一個屬性時,不允許同一個屬性出現(xiàn)兩種以上存取訪問控制。

示例代碼,

var o = {},
    myName = 'erik';
Object.defineProperty(o, 'name', {
    value: myName,
    set: function(name) {
        myName = name;
    },
    get: function() {
        return myName;
    }
});

上面的代碼看起來貌似是沒有什么問題,但是真正執(zhí)行時會報錯,報錯如下,

TypeError: Invalid property.  A property cannot both have accessors and be writable or have a value, #<Object>

因為這里的name屬性同時聲明了value特性和setget特性,這兩者提供了兩種對name屬性的讀寫控制。這里如果不聲明value特性,而是聲明writable特性,結(jié)果也是一樣的,同樣會報錯。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號