Object.observe()帶來(lái)的數(shù)據(jù)綁定變革

2018-06-09 16:21 更新

引言

一場(chǎng)變革即將到來(lái)。Javascript中的一項(xiàng)新特性將會(huì)顛覆之前你對(duì)于數(shù)據(jù)綁定的所有認(rèn)識(shí)。它也將改變你所使用的MVC庫(kù)觀察模型中發(fā)生的修改以及更新的實(shí)現(xiàn)方式。你會(huì)看到,那些所有在意屬性觀察的應(yīng)用性能將會(huì)得到巨大的提升。

我們很高興的看到,Object.observe()已經(jīng)正式加入到了Chrome 36 beta版本中。

Object.observe()是未來(lái)ECMAScript標(biāo)準(zhǔn)之一,它是一個(gè)可以異步觀察Javascript中對(duì)象變化的方法,而無(wú)需使用一個(gè)其他的JS庫(kù)。它允許一個(gè)觀察者接收一個(gè)按照時(shí)間排序的變化記錄序列,這個(gè)序列描述的是一列被觀察的對(duì)象所發(fā)生的變化。


// Let's say we have a model with data
var model = {};
// Which we then observe
Object.observe(model, function(changes){
    // This asynchronous callback runs
    changes.forEach(function(change) {
        // Letting us know what changed
        console.log(change.type, change.name, change.oldValue);
    });
});

當(dāng)被觀察的對(duì)象發(fā)生任何變化時(shí),回調(diào)函數(shù)將會(huì)匯報(bào)這些變化:

通過(guò)使用Object.observe(),你可以不需要使用任何框架就能實(shí)現(xiàn)雙向數(shù)據(jù)綁定。

但這并不意味著你就不應(yīng)該使用一個(gè)框架。對(duì)于一些有著復(fù)雜業(yè)務(wù)邏輯的項(xiàng)目,經(jīng)過(guò)精心設(shè)計(jì)的框架的重要性不言而喻,你應(yīng)該繼續(xù)使用它們。這些框架減輕了開發(fā)新手的工作量,而且只需要編寫很少的代碼就能夠維護(hù)和實(shí)現(xiàn)一些模式并達(dá)到我們想要的目的。如果你不需要一個(gè)框架,你可以使用一個(gè)體積更小,針對(duì)性更強(qiáng)的庫(kù),比如Polymer(它已經(jīng)開始使用Object.observe()了)。

即便你已經(jīng)重度依賴于一個(gè)MV*框架,Object.observe()仍然能為你的應(yīng)用帶來(lái)一些性能各方面的提升,它能夠更快更簡(jiǎn)單的實(shí)現(xiàn)一些功能并維持同樣的API。例如,在Angular以前的一個(gè)Benchmark測(cè)試中,對(duì)于一個(gè)Model中發(fā)生的變化,臟值檢查對(duì)每次更新會(huì)花費(fèi)40ms,而Object.observe()只會(huì)花費(fèi)1-2ms(相當(dāng)于20-40倍的性能提升)。

不需要冗長(zhǎng)代碼來(lái)實(shí)現(xiàn)的數(shù)據(jù)雙向綁定還意味著你不需要通過(guò)輪詢來(lái)發(fā)現(xiàn)變化,這將帶來(lái)更長(zhǎng)的電池使用時(shí)間!

如果你已經(jīng)對(duì)Object.observe()有了一些了解,可以直接跳過(guò)簡(jiǎn)介這一節(jié),或者接著閱讀了解更多關(guān)于它能夠解決的問(wèn)題。

我們要需要觀察什么?

當(dāng)我們?cè)谟懻摂?shù)據(jù)觀察時(shí),我們通常指的是對(duì)一些特定類型的數(shù)據(jù)變化保持關(guān)注:

  • 原始JavaScript對(duì)象中的變化
  • 當(dāng)屬性被添加、改變、或者刪除時(shí)的變化
  • 當(dāng)數(shù)組中的元素被添加或者刪除時(shí)的變化
  • 對(duì)象的原型發(fā)生的變化

數(shù)據(jù)綁定的重要性

當(dāng)你開始關(guān)心模型-視圖的控制分離時(shí),數(shù)據(jù)綁定就會(huì)變成一件重要的事。HTML是一個(gè)非常好的聲明機(jī)制,但是它完全是靜態(tài)的。理想狀態(tài)下,你想要在數(shù)據(jù)和DOM之間聲明它們的關(guān)系,以便讓DOM保持更新。這會(huì)讓你節(jié)省很多由于寫一些重復(fù)代碼而浪費(fèi)的時(shí)間。

當(dāng)你擁有一個(gè)復(fù)雜的用戶界面,你需要理清楚許多數(shù)據(jù)屬性和許多視圖中元素的關(guān)系時(shí),數(shù)據(jù)綁定是非常有用的。這在我們今天需要?jiǎng)?chuàng)建的單頁(yè)應(yīng)用中非常常見。

通過(guò)在瀏覽器中原生的觀察數(shù)據(jù),我們給予了JavaScript框架(或者你編寫的一些功能庫(kù))一種方式來(lái)實(shí)現(xiàn)對(duì)模型數(shù)據(jù)的變化進(jìn)行觀察而不需要依賴于我們今天正在使用的一些hack方法。

現(xiàn)在使用的數(shù)據(jù)綁定方案是什么樣子的

臟值檢查

你以前曾經(jīng)在那里看到過(guò)數(shù)據(jù)綁定?如果你在你的web應(yīng)用中使用過(guò)一個(gè)現(xiàn)代MV*框架(例如Angular,Knockout),那么你或許已經(jīng)使用過(guò)數(shù)據(jù)綁定將數(shù)據(jù)綁定到你的DOM上了。為了復(fù)習(xí)一下,下面是一個(gè)電話列表應(yīng)用的例子,在其中我們會(huì)將一個(gè)phones數(shù)組中的值(在JavaScript中定義)綁定到一個(gè)列表項(xiàng)目中以便于我們的數(shù)據(jù)和UI保持同步。


<html ng-app>
    <head>
        ...
        <script src="angular.js"></script>
        <script src="controller.js"></script>
    </head>
    <body ng-controller="PhoneListCtrl">
        <ul>
            <li ng-repeat="phone in phones">
                {{phone.name}}
                <p>{{phone.snippet}}</p>
            </li>
        </ul>
    </body>
</html>

js的controller如下:


var phonecatApp = angular.module('phonecatApp', []);
phonecatApp.controller('PhoneListCtrl', function($scope) {
    $scope.phones = [
        {
            'name': 'Nexus S',
            'snippet': 'Fast just got faster with Nexus S.'
        }, {
            'name': 'Motorola XOOM with Wi-Fi',
            'snippet': 'The Next, Next Generation tablet.'
        }, {
            'name': 'MOTOROLA XOOM',
            'snippet': 'The Next, Next Generation tablet.'
        }
    ];
});

demo的地址在這里。

在任何時(shí)候,只要是底層的model數(shù)據(jù)發(fā)生了變化,我們?cè)贒OM中的列表也會(huì)跟著更新。Angular是怎么做到這一點(diǎn)的呢?在Angular的背后,有一個(gè)叫做臟值檢查的東西。

臟值檢查的基本原理就是只要任何時(shí)候數(shù)據(jù)發(fā)生了變化,這個(gè)庫(kù)都會(huì)通過(guò)一個(gè)digest或者change cycle去檢查變化是否發(fā)生了。在Angular中,一個(gè)digest循環(huán)意味著所有被監(jiān)視的表達(dá)式都會(huì)被循環(huán)一遍以便查看其中是否有變化發(fā)生。它知道一個(gè)模型之前的值,因此當(dāng)變化發(fā)生時(shí)一個(gè)change事件將會(huì)被觸發(fā)。對(duì)于開發(fā)者來(lái)說(shuō),這帶來(lái)的一大好處就是你可以使用原生的JavaScript對(duì)象數(shù)據(jù),它易于使用及整合。下面的圖片展示的是一個(gè)非常糟糕的算法,它的開銷非常大。

這個(gè)操作的開銷和被監(jiān)視的對(duì)象的數(shù)量是成正比的。我們可能需要做很多的臟治檢查。同時(shí)我也需要一種方式去觸發(fā)臟值檢查,當(dāng)某些數(shù)據(jù)可能發(fā)生改變時(shí)。有很多的框架使用了一些非常聰明的方法來(lái)解決這個(gè)問(wèn)題,但是它們是否足夠好目前還尚無(wú)定論。

web生態(tài)系統(tǒng)應(yīng)該擁有更多的能力去創(chuàng)新和進(jìn)化它自己的聲明機(jī)制,例如:

  • 有約束的模型系統(tǒng)
  • 自動(dòng)的保存系統(tǒng)(例如:將變化保存在IndexedDB或者localStorage中)
  • 容器對(duì)象(Ember,Backbone)

容器對(duì)象是一個(gè)框架創(chuàng)建的對(duì)象,它能夠在其中保存一些數(shù)據(jù)。它們擁有一些存取器去獲取數(shù)據(jù)并且能夠在你設(shè)置或者獲取對(duì)象時(shí)捕獲到這些行為并在內(nèi)部進(jìn)行廣播。這是一種非常好的方式。它的性能很好,從算法上來(lái)說(shuō)也不錯(cuò)。下面是一個(gè)使用Ember容器對(duì)象的一個(gè)簡(jiǎn)單例子:


// Container objects
MyApp.president = Ember.Object.create({
  name: "Barack Obama"
});
MyApp.country = Ember.Object.create({
  // ending a property with "Binding" tells Ember to
  // create a binding to the presidentName property
  presidentNameBinding: "MyApp.president.name"
});
// Later, after Ember has resolved bindings
MyApp.country.get("presidentName");
// "Barack Obama"
// Data from the server needs to be converted
// Composes poorly with existing code

在上面的例子中,發(fā)現(xiàn)什么地方發(fā)生了變化的開銷和發(fā)生改變的東西有著直接聯(lián)系?,F(xiàn)在你存在的另一個(gè)問(wèn)題是你需要使用不同種類的對(duì)象??偟膩?lái)說(shuō)你需要將從服務(wù)器獲取的數(shù)據(jù)進(jìn)行轉(zhuǎn)換以便它們是能夠被觀察到的。

目前的JS代碼并不能很好的整合生成數(shù)據(jù),因?yàn)檫@些代碼一般會(huì)假設(shè)它們操作的是原生JavaScript對(duì)象,而不是一些特定的對(duì)象類似類型。

介紹Object.observe()

我們真正想要的可能是兩個(gè)世界中最好的東西 – 一種支持對(duì)原生數(shù)據(jù)對(duì)象(普通JavaScript對(duì)象)進(jìn)行觀察的方法,同時(shí)不需要每次都對(duì)所有東西進(jìn)行臟值檢查。它需要有良好的算法表現(xiàn)。它還需要能夠很好的整合到各個(gè)平臺(tái)中。這些都是Object.observe()能夠帶給我們的東西。

它允許我們對(duì)一個(gè)對(duì)象或者變異屬性進(jìn)行觀察,并且在變化發(fā)生時(shí)得到及時(shí)通知。但是我們?cè)谶@里不想看什么理論,讓我們來(lái)看看代碼!

Object.observe()和Object.unobserve()

讓我們假設(shè)我們現(xiàn)在有一個(gè)簡(jiǎn)單的JavaScript對(duì)象,它代表一個(gè)模型:


// A model can be a simple vanilla object
var todoModel = {
    label: 'Default',
    completed: false
};

我們可以制定一個(gè)比回調(diào)函數(shù),用來(lái)處理對(duì)象上的變化:


function observer(changes){
    changes.forEach(function(change, i){
        console.log('what property changed? ' + change.name);
        console.log('how did it change? ' + change.type);
        console.log('whats the current value? ' + change.object[change.name]);
        console.log(change); // all changes
    });
}

注意:當(dāng)觀察者回調(diào)函數(shù)被調(diào)用時(shí),被觀察的對(duì)象可能已經(jīng)發(fā)生了多次改變,因此對(duì)于每一次變化,新的值(即每次變化以后的值)和當(dāng)前值(最終的值)并不一定是相同的。

我們可以使用Object.observe()來(lái)觀察這些變化,只要將對(duì)象作為第一個(gè)參數(shù),而將回調(diào)函數(shù)作為第二個(gè)參數(shù):


Object.observe(todoModel, observer);

我們現(xiàn)在對(duì)我們的Todos的模型對(duì)象做一些改變:


todoModel.label = 'Buy some more milk';

看看控制臺(tái),我們現(xiàn)在得到了一些有用的信息!我們知道什么屬性發(fā)生了變化,它是怎樣變化的以及新的值是什么。

再見,臟值檢查!你的墓碑應(yīng)該被刻上Comic Sans字體。我們?cè)賮?lái)改變其他的屬性。這次改變的是completeBy:


todoModel.completeBy = '01/01/2014';

正如我們所見的,我們又再一次得到了關(guān)于變化的報(bào)告:

非常好。要是我們現(xiàn)在決定從對(duì)象中刪除completed屬性會(huì)怎么樣:


delete todoModel.completed;

正如我們所見的,返回的變化報(bào)告包含了關(guān)于刪除的信息。正如我們所期待的,新的值現(xiàn)在是undefined。那么,我們現(xiàn)在知道了你可以知道屬性什么時(shí)候被添加。什么時(shí)候被刪除?;旧蟻?lái)說(shuō),你可以知道一個(gè)對(duì)象上的屬性集(’new’,’deleted’,’recongigured’)以及它的原型(proto)的變化。

在任何觀察系統(tǒng)中,總是存在一個(gè)方法來(lái)停止觀察。在這里,我們有Object.unobserve()方法,它的用法和Object.observe()一樣但是可以像下面一樣被調(diào)用:


Object.unobserve(todoModel, observer);

正如下面所示,在使用該方法之后,任何的變化都不再作為一個(gè)變化列表記錄返回。

指定感興趣的變化

現(xiàn)在我們已經(jīng)了解到了我們?nèi)绾稳カ@取一個(gè)被觀察對(duì)象的變化列表。但是如果我們僅僅只對(duì)一個(gè)對(duì)象中的某些屬性感興趣該怎么辦?人人都需要一個(gè)垃圾郵件過(guò)濾器。Observer可以通過(guò)一個(gè)列表指定一些我們想要看到的變化。我們需要通過(guò)Object.observe()第三個(gè)參數(shù)來(lái)指定:


Object.observe(obj, callback, opt_acceptList)

現(xiàn)在我們來(lái)看一個(gè)如何使用的例子:


// Like earlier, a model can be a simple vanilla object
var todoModel = {
    label: 'Default',
    completed: false
};
// We then specify a callback for whenever mutations
// are made to the object
function observer(changes){
    changes.forEach(function(change, i){
        console.log(change);
    });
};
// Which we then observe, specifying an array of change
// types we’re interested in
Object.observe(todoModel, observer, ['delete']);
// without this third option, the change types provided
// default to intrinsic types
todoModel.label = 'Buy some milk';
// note that no changes were reported

如果我們刪除了這個(gè)標(biāo)簽,注意到這個(gè)類型的變化將會(huì)被報(bào)告:

1
delete todoModel.label;

如果你不指定一個(gè)列表,它默認(rèn)將會(huì)報(bào)告固有的對(duì)象變化類型 (“add”, “update”, “delete”, “reconfigure”, “preventExtensions” (丟與那些不可擴(kuò)展的對(duì)象是不可觀察的))。

通知

Object.observe()也帶有一些通知。它們并不像是你在手機(jī)上看到了通知,而是更加有有用。通知和變異觀察者比較類似。它們發(fā)生在微任務(wù)的結(jié)尾。在瀏覽器的上下文,它幾乎總是位于當(dāng)前事件處理器的結(jié)尾。

這個(gè)時(shí)間點(diǎn)非常的重要因?yàn)榛旧蟻?lái)說(shuō)此時(shí)一個(gè)工作單元已經(jīng)結(jié)束了,現(xiàn)在觀察者已經(jīng)開始它們的共走了。這是一個(gè)非常好的回合處理模型。

使用一個(gè)通知器的工作流程如下所示:

現(xiàn)在我們通過(guò)一個(gè)例子來(lái)如何通過(guò)自定義一個(gè)通知器來(lái)處理一個(gè)對(duì)象的屬性被設(shè)置或者被獲取的情況。注意看代碼中的注釋:


// Define a simple model
var model = {
    a: {}
};
// And a separate variable we'll be using for our model's getter in just a moment
// 定義一個(gè)單獨(dú)的變量,我們即將使用它來(lái)作為我們的模型中的getter
var _b = 2;
// Define a new property 'b' under 'a' with a custom getter and setter
// 在'a'下面定義一個(gè)新的屬性'b',并自定義一個(gè)getter和setter
Object.defineProperty(model.a, 'b', {
    get: function () {
        return _b;
    },
    set: function (b) {
        // Whenever 'b' is set on the model
        // notify the world about a specific type
        // of change being made. This gives you a huge
        // amount of control over notifications
        // 當(dāng)'b'在模型中被設(shè)置時(shí),注意一個(gè)特定類型的變化將會(huì)發(fā)生
        // 這將給你許多關(guān)于通知的控制器
        Object.getNotifier(this).notify({
            type: 'update',
            name: 'b',
            oldValue: _b
        });
        // Let's also log out the value anytime it gets
        // set for kicks
        // 在值發(fā)生變化時(shí)將會(huì)輸出信息
        console.log('set', b);
        _b = b;
    }
});
// Set up our observer
// 設(shè)置我們的觀察者
function observer(changes) {
    changes.forEach(function (change, i) {
        console.log(change);
    })
}
// Begin observing model.a for changes
Object.observe(model.a, observer);

現(xiàn)在當(dāng)數(shù)據(jù)屬性發(fā)生變化時(shí)(‘update’)我們將會(huì)得到報(bào)告。以及任何對(duì)象的實(shí)現(xiàn)也將會(huì)被報(bào)告(notifier.notifyChange())。

多年的web平臺(tái)開發(fā)經(jīng)驗(yàn)告訴我們整合方法是你應(yīng)該最先嘗試的事情,因?yàn)樗钊菀兹?shí)現(xiàn)。但是它存在的問(wèn)題是以它會(huì)創(chuàng)造一個(gè)從根本上來(lái)看就很未下的處理模型。如果你正在編寫代碼并且更新了一個(gè)對(duì)象的屬性,你實(shí)際上并不想陷入這樣一種困境:更新模型中的屬性會(huì)最終導(dǎo)致任意一段代碼去做任意一件事情。當(dāng)你的函數(shù)正好運(yùn)行到一半時(shí),假設(shè)失效并不是什么理想的狀況。

如果你是一個(gè)觀察者,你并不想當(dāng)某人正在做某事的時(shí)候被調(diào)用。你并不像在不連續(xù)的狀態(tài)下被調(diào)用。因?yàn)檫@最終往往會(huì)導(dǎo)致更多的錯(cuò)誤檢查。你應(yīng)該試著去容忍更多的情形,并且基本上來(lái)說(shuō)它是一個(gè)很難去合作的模型。異步是一件更難處理的事情但是最終它會(huì)產(chǎn)生更好的模型。

上述問(wèn)題的解決辦法是變化合成記錄(synthetic change records)。

變化合成記錄

基本上來(lái)說(shuō),如果你想要存取器或者計(jì)算屬性的話,你應(yīng)該復(fù)雜在這些值發(fā)生改變時(shí)發(fā)出通知。這會(huì)導(dǎo)致一些額外的工作,但是它是這種機(jī)制第一類的特征,并且這些通知會(huì)連同來(lái)自余下的底層數(shù)據(jù)對(duì)象的通知一起被發(fā)布出來(lái)。

觀察存取器或者計(jì)算屬性的問(wèn)題可以通過(guò)使用notifier.notify來(lái)解決 – 它也是Object.observe()的另外一部分。大多數(shù)的觀察系統(tǒng)想要某些形式的觀察導(dǎo)出值。有很多方法可以實(shí)現(xiàn)它。Object.observe()并沒有用正確的方式進(jìn)行判斷。計(jì)算屬性應(yīng)該是存取器,當(dāng)內(nèi)部的(私有的)狀態(tài)發(fā)生改變時(shí)它應(yīng)該發(fā)出通知。

再一次聲明,在web中應(yīng)該有一些庫(kù)來(lái)幫助我們進(jìn)行通知并且?guī)椭覀兏玫膶?shí)現(xiàn)計(jì)算屬性(以及減少模板的使用)。

我們?cè)谶@里會(huì)假設(shè)一個(gè)例子,這個(gè)例子中有一個(gè)circle類。在這里,我們有一個(gè)citcle,它有一個(gè)radius屬性。在這里的情形中,radius是一個(gè)存取器,并且當(dāng)它的值發(fā)生變化時(shí)它實(shí)際上會(huì)去通知自己值已經(jīng)發(fā)生變化了。這些通知將會(huì)連同其他變化被傳遞到這個(gè)對(duì)象或者其他對(duì)象。本質(zhì)上來(lái)說(shuō),如果你正在實(shí)現(xiàn)一個(gè)對(duì)象,你一定會(huì)想要擁有整合或者計(jì)算屬性的對(duì)象,或者你想要想出一個(gè)策略如何讓它運(yùn)行。一旦你做了這件事,它將會(huì)適應(yīng)你的整個(gè)系統(tǒng)。

看看下面的代碼在開發(fā)者工具中是如何運(yùn)行的:


function Circle(r) {
  var radius = r;
  var notifier = Object.getNotifier(this);
  function notifyAreaAndRadius(radius) {
    notifier.notify({
      type: 'update',
      name: 'radius',
      oldValue: radius
    })
    notifier.notify({
      type: 'update',
      name: 'area',
      oldValue: Math.pow(radius * Math.PI, 2)
    });
  }
  Object.defineProperty(this, 'radius', {
    get: function() {
      return radius;
    },
    set: function(r) {
      if (radius === r)
        return;
      notifyAreaAndRadius(radius);
      radius = r;
    }
  });
  Object.defineProperty(this, 'area', {
    get: function() {
      return Math.pow(radius, 2) * Math.PI;
    },
    set: function(a) {
      r = Math.sqrt(a/Math.PI);
      notifyAreaAndRadius(radius);
      radius = r;
    }
  });
}
function observer(changes){
  changes.forEach(function(change, i){
    console.log(change);
  })
}

存取器屬性

在這里我們對(duì)于存取器屬性有一個(gè)簡(jiǎn)短的提示。在前面我們提到了對(duì)于數(shù)據(jù)屬性來(lái)說(shuō)只有值得變化是能夠被觀察到的。而存取器屬性和計(jì)算屬性則無(wú)法被觀察到。這是因?yàn)镴avaScript中的存取器并沒有真正的值的變化。一個(gè)存取器僅僅是一個(gè)函數(shù)集合。

如果你為一個(gè)存取器屬性賦值,你僅僅只是調(diào)用了這個(gè)函數(shù),并且在它看來(lái)值并沒有發(fā)生變化。它僅僅只是讓一些代碼運(yùn)行起來(lái)。

這里的問(wèn)題在于我們?cè)谏厦娴睦又袑⒋嫒∑鲗傩再x值為5.我們應(yīng)該能夠知道這里究竟發(fā)生了什么。這實(shí)際上是一個(gè)未解決的問(wèn)題。這個(gè)例子說(shuō)明了原因。對(duì)任何系統(tǒng)來(lái)說(shuō)知道這究竟意味著什么是不可能的,因?yàn)樵谶@里可以運(yùn)行任意代碼。每當(dāng)存取器屬性被訪問(wèn)時(shí),它的值都會(huì)發(fā)生改變,因此詢問(wèn)它什么時(shí)候會(huì)發(fā)生變化并沒有多大的意義。

使用一個(gè)回調(diào)函數(shù)觀察多個(gè)對(duì)象

Object.observe()上的另一個(gè)模式是使用單個(gè)回調(diào)觀察者。這允許我們使用同一個(gè)回調(diào)函數(shù)堆多個(gè)不同的對(duì)象進(jìn)行觀察。這個(gè)回調(diào)函數(shù)在“微任務(wù)”的結(jié)尾將會(huì)把所有的變化都傳遞給它所觀察的對(duì)象。

大規(guī)模的變化

也許你正在編寫一個(gè)非常大的應(yīng)用,并且經(jīng)常需要處理大規(guī)模的變化。此時(shí)我們希望用一種更加緊湊的方式來(lái)描述影響很多屬性的語(yǔ)義變化。

Object.observe()使用兩個(gè)特定的函數(shù)來(lái)解決這個(gè)問(wèn)題:notifier.performChange()以及notifier.notify(),我們?cè)谏厦嬉呀?jīng)介紹過(guò)這兩個(gè)函數(shù)了。

我們可以從下面的例子中看到我們?nèi)绾蝸?lái)描述大規(guī)模變化,在這個(gè)例子中定義了一個(gè)叫做Thingy的對(duì)象,其中包含幾個(gè)數(shù)計(jì)算功能(multiply, increment, incrementAndMultiply)。只要其中一個(gè)功能被使用,它就會(huì)告訴系統(tǒng)一些包含特定變化的事情發(fā)生了。

例如: notifier.performChange(‘foo’, performFooChangeFn)


function Thingy(a, b, c) {
  this.a = a;
  this.b = b;
}
Thingy.MULTIPLY = 'multiply';
Thingy.INCREMENT = 'increment';
Thingy.INCREMENT_AND_MULTIPLY = 'incrementAndMultiply';
Thingy.prototype = {
  increment: function(amount) {
    var notifier = Object.getNotifier(this);
    // Tell the system that a collection of work comprises
    // a given changeType. e.g
    // notifier.performChange('foo', performFooChangeFn);
    // notifier.notify('foo', 'fooChangeRecord');
    notifier.performChange(Thingy.INCREMENT, function() {
      this.a += amount;
      this.b += amount;
    }, this);
    notifier.notify({
      object: this,
      type: Thingy.INCREMENT,
      incremented: amount
    });
  },
  multiply: function(amount) {
    var notifier = Object.getNotifier(this);
    notifier.performChange(Thingy.MULTIPLY, function() {
      this.a *= amount;
      this.b *= amount;
    }, this);
    notifier.notify({
      object: this,
      type: Thingy.MULTIPLY,
      multiplied: amount
    });
  },
  incrementAndMultiply: function(incAmount, multAmount) {
    var notifier = Object.getNotifier(this);
    notifier.performChange(Thingy.INCREMENT_AND_MULTIPLY, function() {
      this.increment(incAmount);
      this.multiply(multAmount);
    }, this);
    notifier.notify({
      object: this,
      type: Thingy.INCREMENT_AND_MULTIPLY,
      incremented: incAmount,
      multiplied: multAmount
    });
  }
}

我們可以為我們的對(duì)象定義兩個(gè)觀察者: 一個(gè)用來(lái)捕獲所有的變化,另一個(gè)將只會(huì)匯報(bào)我們定義的特定類型的變化 (Thingy.INCREMENT, Thingy.MULTIPLY, Thingy.INCREMENTANDMULTIPLY)。


var observer, observer2 = {
    records: undefined,
    callbackCount: 0,
    reset: function() {
      this.records = undefined;
      this.callbackCount = 0;
    },
};
observer.callback = function(r) {
    console.log(r);
    observer.records = r;
    observer.callbackCount++;
};
observer2.callback = function(r){
trueconsole.log('Observer 2', r);
}
Thingy.observe = function(thingy, callback) {
  // Object.observe(obj, callback, optAcceptList)
  Object.observe(thingy, callback, [Thingy.INCREMENT,
                                    Thingy.MULTIPLY,
                                    Thingy.INCREMENT_AND_MULTIPLY,
                                    'update']);
}
Thingy.unobserve = function(thingy, callback) {
  Object.unobserve(thingy);
}

我們現(xiàn)在可以開始玩弄一下代碼了。我們先定義一個(gè)新的Thingy


var thingy = new Thingy(2,4);

對(duì)它進(jìn)行觀察并進(jìn)行一些變化。有趣的事情發(fā)生了!


// Observe thingy
Object.observe(thingy, observer.callback);
Thingy.observe(thingy, observer2.callback);
// Play with the methods thingy exposes
thingy.increment(3);               // { a: 5, b: 7 }
thingy.b++;                        // { a: 5, b: 8 }
thingy.multiply(2);                // { a: 10, b: 16 }
thingy.a++;                        // { a: 11, b: 16 }
thingy.incrementAndMultiply(2, 2); // { a: 26, b: 36 }

位于這個(gè)perform function中的一切東西都可以被看作是大型變化進(jìn)行的工作。接受大型變化的觀察者僅僅只會(huì)接受大型變化”記錄。那些不會(huì)接受底層變化的觀察者都來(lái)源于perform function所做的事。

觀察數(shù)組

我們已經(jīng)討論了如何觀察一個(gè)對(duì)象,但是應(yīng)該如何觀察數(shù)組呢?

Array.observe()是一個(gè)針對(duì)自身大型變化的方法 – 例如 – splice,unshift或者任何能夠隱式影響數(shù)組長(zhǎng)度的東西。在內(nèi)部它使用了notifier.performChange(“splice”,…)。

下面是一個(gè)我們?nèi)绾斡^察一個(gè)模型數(shù)組的例子,當(dāng)?shù)讓訑?shù)據(jù)發(fā)生一些變化時(shí),我們將能夠得到一個(gè)變化的列表。


var model = ['Buy some milk', 'Learn to code', 'Wear some plaid'];
var count = 0;
Array.observe(model, function(changeRecords) {
  count++;
  console.log('Array observe', changeRecords, count);
});
model[0] = 'Teach Paul Lewis to code';
model[1] = 'Channel your inner Paul Irish';

性能

考慮Object.observe()性能的方式是將它想成讀緩存?;旧蟻?lái)說(shuō),在以下幾種情形中,一個(gè)緩存是最佳選擇(按照重要性排序):

  1. 讀的頻率決定著寫的頻率
  2. 你可以創(chuàng)造一個(gè)緩存,它可以在讀數(shù)據(jù)期間將涉及到寫數(shù)據(jù)的操作進(jìn)行算法上的優(yōu)化
  3. 寫數(shù)據(jù)減慢的時(shí)間常數(shù)是可以接受的

Object.observe()是為上述第一種情形設(shè)計(jì)的。

臟值檢查需要保留一個(gè)你所要觀察數(shù)據(jù)的副本。這意味著在臟值檢查中你需要一個(gè)額外的結(jié)構(gòu)內(nèi)存開銷。臟值檢查,一個(gè)作為權(quán)宜之計(jì)的解決方案,同時(shí)根本上也是一個(gè)脆弱的抽象,它可能會(huì)導(dǎo)致應(yīng)用中一些不必要的復(fù)雜性。

臟值檢查在任何數(shù)據(jù)可能發(fā)生變化的時(shí)候都必須要運(yùn)行。這很明顯并不是一個(gè)非常魯棒的方法,并且任何實(shí)現(xiàn)臟值檢查的途徑都是有缺陷的(例如,在輪詢中進(jìn)行檢查可能會(huì)造成視覺上的假象以及涉及到代碼的紊亂情況)。臟值檢查也需要注冊(cè)一個(gè)全局的觀察者,這很可能會(huì)造成內(nèi)存泄漏,而Object.observe()會(huì)避免這一點(diǎn)。

我們現(xiàn)在來(lái)看一些數(shù)據(jù)。

下面的基準(zhǔn)測(cè)試允許我們比較臟值檢查Object.observe()。圖中比較的數(shù)據(jù)是Observed-Object-Set-SizeNumber-Of-Mutations。

總的結(jié)果表明:臟值檢查的性能和被觀察的對(duì)象成正比,而``Object.observe()的性能和我們所做的改變成正比。

Dirty-checking

Chrome with Object.observe() switched on

為Object.observe()提供墊片

Object.observe()現(xiàn)在已經(jīng)可以在Chrome 36 beta中使用,但是如果我們想要在其他瀏覽器中使用它該怎么辦?Polymer中的Observe-JS是一個(gè)針對(duì)于那些沒有原生實(shí)現(xiàn)Object.observe()瀏覽器的一個(gè)墊片,但是它不僅僅是作為墊片,同時(shí)也包含了許多有用的語(yǔ)法糖。它提供了一種整合的視角,它能夠?qū)⑺凶兓偨Y(jié)起來(lái)并且提交一份關(guān)于變化的報(bào)告。它的好處主要體現(xiàn)在兩點(diǎn):

  1. 你可以觀察路徑。這意味著你可以說(shuō),我想要從一個(gè)給定的對(duì)象中觀察foo.bar.baz,只要這個(gè)路徑的值發(fā)生了改變,你會(huì)得到通知。如果路徑是錯(cuò)誤的,將會(huì)返回undefined。

下面是一個(gè)例子:


  1. var obj = { foo: { bar: 'baz' } };
    var observer = new PathObserver(obj, 'foo.bar');
    observer.open(function(newValue, oldValue) {
      // respond to obj.foo.bar having changed value.
    });
  2. 能夠告訴你數(shù)組的拼接。數(shù)組拼接基本上來(lái)說(shuō)是你為了將舊版本數(shù)組轉(zhuǎn)換為新版本數(shù)組是需要進(jìn)行了最基本的拼接操作。這是一種轉(zhuǎn)換的類型或者是這個(gè)數(shù)組的不同視圖。它是你想要將數(shù)組從舊狀態(tài)變?yōu)樾聽顟B(tài)時(shí)需要進(jìn)行的最基本的工作。

下面是一個(gè)例子


var arr = [0, 1, 2, 4];
var observer = new ArrayObserver(arr);
observer.open(function(splices) {
  // respond to changes to the elements of arr.
  splices.forEach(function(splice) {
    splice.index; // index position that the change occurred.
    splice.removed; // an array of values representing the sequence of elements which were removed
    splice.addedCount; // the number of elements which were inserted.
  });
});

框架和Object.observe()

正如上面所提到的,使用Object.observe()能夠給予框架和庫(kù)中關(guān)于數(shù)據(jù)綁定的性能巨大的提升。

來(lái)自Ember的Yehuda Katz和Erik Bryn已經(jīng)確定將會(huì)在Ember最近的修改版本中添加對(duì)Object.observe()的支持。來(lái)自Angular的Misko Hervy寫了一份關(guān)于Angular 2.0的設(shè)計(jì)文檔,其中的內(nèi)容關(guān)于改善變化探測(cè)(change detection)。在將來(lái),當(dāng)Object.observe()在Chrome穩(wěn)定版中出現(xiàn)時(shí),Angular會(huì)使用Object.observe()來(lái)實(shí)現(xiàn)變化探測(cè)的功能,在此之前它們會(huì)選擇使用Watchtower.js – Angular自己的變化探測(cè)的實(shí)現(xiàn)方式。實(shí)在是太令人激動(dòng)了。

總結(jié)

Object.observe()是一個(gè)添加到web平臺(tái)上非常強(qiáng)大的特性,你現(xiàn)在就可以開始使用它。

我們希望這項(xiàng)特征能夠及時(shí)的登陸到更多的瀏覽器中,它能夠允許JavaScript框架從本地對(duì)象觀察的能力中獲得更多性能上的提升。Chrome 36 beta及其以上的版本都能使用這項(xiàng)特性,在未來(lái)Opera發(fā)布的版本中這項(xiàng)特性也會(huì)得到支持。

現(xiàn)在就和JavaScript框架作者談?wù)?code>Object.observe()如何能夠提高他們框架中數(shù)據(jù)綁定的性能。未來(lái)還有更多讓人激動(dòng)的時(shí)刻。

參考鏈接



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

掃描二維碼

下載編程獅App

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

編程獅公眾號(hào)