簡介
革命即將到來。JavaScript 新增了一個新功能,將改變您對資料繫結的一切認知。這也會改變許多 MVC 程式庫如何處理觀察模型的編輯和更新作業。您是否已準備好為關心屬性觀察的應用程式提供優異的效能提升功能?
好的,好了,很高興在此宣布,Object.observe()
已成為 Chrome 36 穩定版。[WOOOO. The CROWD GOES WILD]。
Object.observe()
是未來 ECMAScript 標準的一部分,是一種以非同步方式觀測 JavaScript 物件變更的方法,不需要使用獨立的程式庫。這可讓觀察器接收按時間排序的變更記錄序列,這些記錄會描述一組觀察物件發生的變更。
// 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);
});
});
只要使用者做出變更,系統就會回報以下內容:
使用 Object.observe()
(我喜歡將其稱為 O.o() 或 Oooooooo),可讓您不必使用架構,就能實作雙向資料繫結。
但這並不代表您不應使用。對於包含複雜業務邏輯的大型專案,有主見的架構非常實用,您應繼續使用這些架構。這類工具可簡化新開發人員的方向,減少程式碼維護作業,並提供常見工作模式。不需要時,您可以使用較小型且較明確的程式庫,例如 Polymer (目前已使用 O.o() 的 O.o())。
即使您大量使用架構或 MV* 程式庫,O.o() 仍可為這些項目提供一些良好的效能改善,並在保留相同 API 的情況下,實作更快速、更簡單的實作方式。舉例來說,Angular 去年發現,在對模型進行變更的基準測試中,髒值檢查每更新一次需要 40 毫秒,而 O.o() 每更新一次需要 1 到 2 毫秒 (速度提升 20 到 40 倍)。
資料繫結不需要大量複雜的程式碼,也代表您不再需要輪詢變更,因此電池續航力可延長!
如果你已透過 O.o() 販售產品,請跳到功能簡介,或是閱讀完整說明,進一步瞭解這項功能可以解決的問題。
我們想觀察什麼?
談到資料觀察,我們通常是指留意某些特定類型的變化:
- 原始 JavaScript 物件的變更
- 新增、變更及刪除屬性時
- 陣列中的元素有插入和移除的情況
- 物件原型變更
資料繫結的重要性
當您在意模型-檢視控制項分離時,資料繫結就會變得重要。HTML 是絕佳的宣告式機制,但完全靜態。在理想情況下,您只需宣告您的資料與 DOM 之間的關係,並隨時將 DOM 保持在最新狀態。這樣一來,您就能善用這項功能,省下許多時間來編寫重複的程式碼,這些程式碼只會在應用程式的內部狀態或伺服器之間傳送資料。
如果是複雜的使用者介面,需要在檢視畫面中使用多個元素連結資料模型中多個資源之間的關係,資料繫結功能就特別實用。在目前建構的單一頁面應用程式中,這種情況相當常見。
透過烘焙出在瀏覽器中觀測資料的方法,和您編寫的小型公用程式程式庫,我們得以讓 JavaScript 架構 (和您編寫的小型公用程式程式庫) 觀察模型資料的變化,而無需依賴現今一些全球緩慢的駭客攻擊手法。
今日世界樣貌
信用檢查
您之前曾在哪裡看過資料繫結?如果您使用現代 MV* 程式庫來建構網頁應用程式 (例如 Angular、Knockout),您可能會習慣將模型資料與 DOM 繫結在一起。讓我們再複習一下,在以下示例中,手機清單應用程式會將 phones
陣列 (在 JavaScript 中定義) 中每個手機的值繫結至清單項目,讓資料和 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'>
<p></p>
</li>
</ul>
</body>
</html>
以及控制器的 JavaScript:
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.'}
];
});
只要基礎模型資料有所變更,DOM 中的清單就會隨之更新。Angular 如何達成這一點?在幕後,它會執行所謂的髒值檢查。
執行髒汙檢查的基本概念,是只要資料可能有所變更,程式庫就必須透過摘要或變化週期檢查該變更是否發生變化。在 Angular 的情況下,摘要週期會找出所有註冊的要監控的運算式,以便查看是否有變更。知道模型先前的值;如果變更,就會觸發變更事件。對開發人員而言,這麼做的主要好處是您可以使用原始的 JavaScript 物件資料,而且非常適合使用和撰寫。缺點是演算法的行為不理想,且費用可能非常高。
此作業的費用與觀察到的物件總數成正比。我可能需要檢查大量髒汙。此外,在資料「可能」出現變動時,可能也需要觸發骯髒檢查的方法。框架會使用許多聰明的訣竅來達成這項目標。目前尚不清楚這是否會是完美的解決方案。
網路生態系統應該有更多能力創新及發展自家的宣告式機制,例如:
- 以限制為準的模型系統
- 自動持久化系統 (例如將變更持久化至 IndexedDB 或 localStorage)
- 容器物件 (Ember、Backbone)
容器物件是指架構建立的物件,這些物件會在內部儲存資料。這些類別具有資料存取權,可擷取您設定或取得的資料,並在內部播送。這麼做很有效。這項功能效能相對出色,且具備良好的演算法行為。以下是使用 Ember 的容器物件範例:
// 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
發現此部分改變的花費與改變的內容數量成正比。另一個問題是,您已使用這種不同類型的物件。一般而言,您必須將資料從伺服器取得的資料轉換為這些物件,以便查看。
這與現有的 JS 程式碼不太相容,因為大多數程式碼都假設可以處理原始資料。不適用於這類特殊物件。
Introducing Object.observe()
最理想的狀況是兩全其美,當我們選擇使用「且」不需要時,檢查所有內容的情況下,這個方式可以使用原始資料物件 (一般 JavaScript 物件) 來觀測資料。具有良好演算法行為的內容。可妥善組合且已整合至平台的內容。這就是 Object.observe()
的優勢。
這可讓我們觀察物件、變更屬性,並查看變更報告。但理論講得夠多了,讓我們來看看程式碼吧!
Object.observe() 和 Object.unobserve()
假設我們有一個簡單的一般 JavaScript 物件,用來代表模型:
// A model can be a simple vanilla object
var todoModel = {
label: 'Default',
completed: false
};
接著,我們可以指定回呼,以便在物件發生變異 (變更) 時執行:
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
});
}
接著,我們可以使用 O.o() 來觀察這些變化,然後傳入 物件做為第一個引數,並將回呼當做第二個引數:
Object.observe(todoModel, observer);
現在來變更 Todos 模型物件:
todoModel.label = 'Buy some more milk';
查看控制台後,我們獲得了一些實用資訊!我們會知道哪些屬性已變更、變更方式為何,以及新值為何。
太棒了!再見,仔細檢查再接再厲!您的墓碑應以 Comic Sans 字型刻字。接著,我們來變更其他屬性。這次 completeBy
:
todoModel.completeBy = '01/01/2014';
我們可再次成功傳回變更報表,說明如下:
麻煩了!如果現在決定從物件中刪除「completed」屬性,會有什麼影響:
delete todoModel.completed;
如您所見,系統傳回的變更報表會包含刪除作業相關資訊,如預期,屬性的新值現在未定義。如此一來,您便能得知何時新增屬性。刪除時間。基本上,物件上的屬性集 (「新增」、「已刪除」、「已重新設定」) 及其原型會變更 (proto)。
如同任何觀察系統,也存在停止監聽變更的方法。在本例中為 Object.unobserve()
,其簽章與 O.o() 相同,但可以按照下列方式呼叫:
Object.unobserve(todoModel, observer);
如以下所示,在執行此操作後,對物件所做的任何變異不會再導致變更記錄清單傳回。
指定興趣變更
我們已瞭解如何取得觀察物件變更清單的基礎知識。如果您只對物件上部分變更感興趣,而非所有變更,該怎麼辦?所以每個人都需要垃圾郵件篩選器。您可以讓觀察者透過接受清單,指定他們想透過接受清單來聽取的變更類型。使用 O.o() 的第三個引數即可指定此值,如下所示:
Object.observe(obj, callback, optAcceptList)
以下舉例說明使用規則:
// 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
不過,如果現在刪除標籤,您會發現系統會回報這類變更:
delete todoModel.label;
如果您未指定 O.o() 的接受類型清單,系統會預設為「固有」物件變更類型 (add
、update
、delete
、reconfigure
、preventExtensions
(用於無法觀測物件無法擴充的情況)。
通知
O.o() 也提供通知概念。這些通知不會像手機上的通知那樣令人煩心,反而相當實用。通知類似於 Mutation Observers。這些事件會在微型工作結束時發生。在瀏覽器環境中,這幾乎一律會位於目前事件處理常式的結尾。
這麼做很適合,因為通常一個工作單元完成後,觀察者就能執行工作。這是很好的回合式處理模型
使用通知器的工作流程大致如下所示:
以下舉例說明如何在實務中,使用通知器定義自訂通知,以在取得或設定物件屬性時接收通知。請留意以下留言:
// Define a simple model
var model = {
a: {}
};
// And a separate variable we'll be using for our model's
// getter in just a moment
var _b = 2;
// Define a new property 'b' under 'a' with a custom
// getter and 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
Object.getNotifier(this).notify({
type: 'update',
name: 'b',
oldValue: _b
});
// Let's also log out the value anytime it gets
// set for kicks
console.log('set', b);
_b = b;
}
});
// Set up our observer
function observer(changes) {
changes.forEach(function (change, i) {
console.log(change);
})
}
// Begin observing model.a for changes
Object.observe(model.a, observer);
我們會在此回報資料屬性值的變更情形 (即「更新」)。物件實作項目選擇回報的其他項目 (notifier.notifyChange()
)。
我們在網路平台上累積了多年的經驗,因此建議您一開始嘗試同步方法,因為這類方法最容易理解。問題是,這會建立根本上危險的處理模型。如果您正在編寫程式碼,並且更新物件的屬性,您其實不希望更新該物件的屬性時,會邀請任意程式碼來執行任何所需的程式碼。在函式執行過程中,假設條件無效並不理想。
如果您是觀察者,最好不要在畫面中間有人發言。您不希望系統要求您在不一致的狀態下執行工作。最終會進行更多錯誤檢查。要容忍更多不良情況,而且通常很難處理。雖然非同步做法較難處理,但用在一天結束後,模型的成效就會更好。
解決這個問題的方法是使用綜合變更記錄。
綜合變更記錄
基本上,如果您想使用存取子或計算的屬性,則有責任在這些值變更時通知您。這項工作雖然需要額外付出一些心力,但這項機制設計為一項一級功能,這些通知會與基礎資料物件的其他通知一併傳送。從資料屬性。
如要觀察存取子和計算屬性,可以使用 notifier.notify,這是 O.o() 的另一個部分。大多數觀察系統都需要以某種形式觀察衍生值。方法有很多種,O.o 不會判斷「正確」的做法。計算屬性應為存取工具,可在內部 (私人) 狀態變更時通知。
同樣地,Webdev 應預期程式庫可協助您輕鬆通知運算屬性和各種方法,並減少樣板。
讓我們設定下一個範例,也就是圓形類別。這裡的概念是,我們有這個圓形和半徑屬性。在本例中,半徑是存取子,當其值變更時,它會通知自身值已變更。這個物件會與這個物件或任何其他物件的其他變更一併傳送。基本上,實作物件時,需要有合成或計算屬性,或必須挑選運作方式。這樣一來,您的整體系統就能套用這項變更。
略過程式碼,看看這項功能在開發人員工具中如何運作。
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);
})
}
存取子屬性
關於存取子屬性,有幾點需要特別注意。我們先前提過,只有資料屬性可觀測的值變更。不適用於運算屬性或存取子。原因是 JavaScript 並沒有「存取子」的值變更概念。存取子只是一組函式。
如果您將其指派給存取工具,JavaScript 就會在該處叫用函式,從其角度來看,沒有任何變化。只是讓部分程式碼有機會執行。
這個問題在語意上是,我們可以查看上述的指定值 - 5 的值。我們應該可以知道發生了什麼事。這其實是無法解決的問題。這個範例說明瞭原因。任何系統都無法得知這段程式碼的含意,因為這可能是任意程式碼。在這種情況下,它可以執行任何操作。因為系統會在每次存取其值時更新該值,因此詢問是否可以變更其值,無意義。
使用單一回呼觀察多個物件
O.o() 提供的另一種模式,是單一回呼觀察器的概念。這使單一回呼可以做為許多不同物件的「觀察器」。回呼會在「微型工作結束」時,將完整的變更集合傳送至觀察到的所有物件 (請注意,這與突變觀察器相似)。
大規模變更
也許您正在開發的應用程式相當龐大,而且經常需要進行大規模的變更。物件可能會以更精簡的方式描述會影響大量屬性的大型語意變更 (而非廣播大量屬性變更)。
O.o() 可透過兩個特定公用程式來協助解決這個問題:notifier.performChange()
和 notifier.notify()
,這是先前引進的公用程式。
以下舉例說明,在定義 Thingy 物件時,我們使用一些數學公用程式 (乘上、遞增、遞增和遞增) 定義大規模變更。只要使用公用程式,系統就會告知系統,該工作組包含特定類型的變更。
例如: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
});
}
}
接著,我們為物件定義兩個觀察器:一個是變更的萬用方法,另一個則只會回報我們定義的特定接受類型 (Thingy.INCREMENT、Thingy.MULTIPLY、Thingy.INCREMENT_AND_MULTIPLY)。
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){
console.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);
}
我們現在可以開始使用這個程式碼了。讓我們定義新的 Thingy:
var thingy = new Thingy(2, 4);
觀察後再進行調整。我的天,很有趣。好多東西!
// 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 }
「perform function」內的所有內容都視為「big-change」的工作。接受「big-change」的觀察器只會收到「big-change」記錄。未執行此操作的觀測器不會收到「執行函式」所執行工作所導致的基礎變更。
觀察陣列
我們討論過如何觀察物件變化,但陣列呢?好問題。當有人對我說「Great question」時,我從沒聽到他們的答案,因為自己忙著問自己一個好問題,但是我很期待。我們也推出了用於處理陣列的新方法!
Array.observe()
是一種方法,可將自身的大量變更 (例如 splice、unshift 或任何會隱含變更長度的操作) 視為「splice」變更記錄。在內部使用 notifier.performChange("splice",...)
。
在以下例子中,我們觀察到模型「陣列」,而且基礎資料發生變更時,系統也能用類似方式取回變更清單:
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';
成效
如要瞭解 O.o() 對運算效能造成的影響,請將其視為讀取快取。一般來說,在下列情況下,快取是個不錯的選擇 (依重要性排序):
- 讀取頻率佔據寫入頻率。
- 您可以建立快取來交易寫入期間持續耗用的工作量,透過演算法提升讀取效能。
- 寫入作業可以持續放慢速度。
O.o() 是為了 1) 這類用途而設計。
髒值檢查需要保留您觀察到的所有資料副本。這表示 O.o() 無法讓您檢查結構記憶體,但執行髒汙檢查卻也是一種基本上外洩的抽象化機制,可能會對應用程式造成不必要的複雜性。
這是因為髒值檢查必須在資料「可能」變更時執行。單靠它並沒有非常穩健的方法,而任何方法都有明顯的缺點 (例如檢查輪詢間隔,可能會導致視覺產失和程式碼疑慮之間的競爭狀況)。骯髒檢查也需要觀察器的全域註冊資料庫,因此會產生記憶體流失風險和拆解成本,而 O.o() 則會避免。
讓我們來看看一些數據。
下列基準測試 (可在 GitHub 取得) 讓我們可以比較骯髒檢查與 O.o() 的異同。它們的結構為 Observed-Object-Set-Size 和 Mutations 的圖表。一般而言,骯髒檢查效能與觀察到的物件數量成正比,而 O.o() 的效能與所發生的變動數量成正比。
信用檢查
已開啟 Object.observe() 的 Chrome
用 Object.observe() 進行 Polyfilling
太好了!您可以在 Chrome 36 中使用 O.o(),但如何在其他瀏覽器中使用 O.o()?請放心,我們會提供協助。Polymer 的 Observe-JS 是 O.o() 的 polyfill,如果有原生實作項目,就會使用該實作項目,否則會進行 polyfill,並在上面加入一些實用的 sugaring。透過這個資訊主頁,您可以掌握世界各地的匯總資料,以及提供變更項目報表。它公開有兩個強大的功能:
- 您可以觀察路徑。也就是說,您可以說:「我想觀察特定物件中的 "foo.bar.baz"」,系統就會在該路徑的值變更時通知您。如果無法存取路徑,系統會將值視為未定義。
觀察特定物件路徑中值的範例:
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.
});
- 它會告訴您如何進行陣列拼接。陣列拼接基本上是指您必須對陣列執行的最少拼接作業集,才能將舊版陣列轉換為新版陣列。這是轉換類型,或是陣列的不同檢視模式。這也是從舊狀態移至新狀態需要執行的最低工作量。
將陣列回報為最小的片段集合變更示例:
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()
如先前所述,O.o() 將為架構和程式庫帶來絕佳的機會,協助他們在支援此功能的瀏覽器中改善資料繫結的效能。
Ember 的 Yehuda Katz 和 Erik Bryn 已確認,在 Ember 近期的路線圖中,會新增對 O.o() 的支援。Angular 的 Misko Hervy 撰寫了一篇設計文件,說明 Angular 2.0 改善的變更偵測功能。他們的長期做法是,在 Object.observe() 穩定版推出後,選擇使用 Watchtower.js,也就是他們自己的變更偵測方法。太棒了!
結論
O.o() 是網路平台的強大功能,您可以立即使用並開始使用。
我們希望這項功能未來能在更多瀏覽器中推出,讓 JavaScript 架構提高使用原生物件觀察功能的效能。指定 Chrome 瀏覽器應該能在 Chrome 36 以上版本中使用 O.o(),而在未來的 Opera 版本中,應該也會提供這項功能。
因此,建議您與 JavaScript 架構作者討論 Object.observe()
,並說明他們計劃如何利用這個架構提升應用程式內資料繫結的成效。未來一定會有更多精彩的內容!
資源
- Harmony 維基上的 Object.observe()>
- Rick Waldron 的「使用 Object.observe() 進行資料繫結」
- Object.observe() 的所有相關須知 - JSConf
- 為何 Object.observe() 是 ES7 的最佳功能
感謝 Rafael Weinstein、Jake Archibald、Eric Bidelman、Paul Kinlan 和 Vivian Cromwell 提供意見和評論。