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

2018-06-09 18:00 更新

緣起

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

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

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

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

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

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

臟檢查

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

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

觀察機(jī)制

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

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

封裝屬性訪問器

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

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

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

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

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

Object.defineProperty方法

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

我們先來MDN上對(duì)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方法提供了一種直接的方式來定義對(duì)象屬性或者修改已有對(duì)象屬性。其方法原型如下,

Object.defineProperty(obj, prop, descriptor)

其中,

  • obj,待修改的對(duì)象
  • prop,帶修改的屬性名稱
  • descriptor,待修改屬性的相關(guān)描述

descriptor要求傳入一個(gè)對(duì)象,其默認(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包括:是否可以對(duì)屬性進(jìn)行重新賦值。
  4. value,屬性的默認(rèn)值。
  5. set,屬性的重寫器(暫且這么叫)。一旦屬性被重新賦值,此方法被自動(dòng)調(diào)用。
  6. get,屬性的讀取器(暫且這么叫)。一旦屬性被訪問讀取,此方法被自動(dòng)調(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. 因?yàn)閍ge屬性是可重寫的
console.log(Object.keys(o)); // []. name和age屬性都不是可枚舉的
Object.defineProperty(o, 'sex', {
    value: 'male',
    writable: false
});
o.sex = 'female'; // 這里的賦值其實(shí)是不起作用的
console.log(o.sex); // 'male';
delete o.sex; // false, 屬性刪除的動(dòng)作也是無效的

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

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

因?yàn)?code>Object.defineProperty()在聲明一個(gè)屬性時(shí),不允許同一個(gè)屬性出現(xiàn)兩種以上存取訪問控制。

示例代碼,

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

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

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

因?yàn)檫@里的name屬性同時(shí)聲明了value特性和setget特性,這兩者提供了兩種對(duì)name屬性的讀寫控制。這里如果不聲明value特性,而是聲明writable特性,結(jié)果也是一樣的,同樣會(huì)報(bào)錯(cuò)。


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

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)