引言
革命即將來臨。為此,我們新增了 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() 或 Oooooo),您可以實作雙向資料繫結,「不需使用架構」。
當然,這不代表你不應該使用這種價值。對於具有複雜商業邏輯的大型專案,明確的架構非常有用,因此建議您繼續使用。這類模型可以簡化新開發人員的螢幕方向、減少維護程式碼,並針對如何執行常見任務施加模式。當您不需要時,您可以使用範圍更小、更集中的程式庫,例如 Polymer (已利用 O.o())。
即使您大量使用架構或 MV* 程式庫,O.o() 仍有機會在保有相同 API 的情況下,以更快、更簡單的實作方式,改善效能。舉例來說,去年 Angular 發現,在基準中對模型進行變更時,進行骯髒檢查需要 40 毫秒,而 O.o() 每次更新需要 1 到 2 毫秒 (改善速度快了 20 到 40 倍)。
有了資料繫結,您就不必對變更進行輪詢,也能延長電池續航力!
如果您已經透過 O.o() 銷售產品,請直接跳至功能介紹,或預先閱讀問題解決方式。
我們想瞭解什麼?
我們所說的資料觀察,通常是指密切留意幾種特定類型的變化:
- 原始 JavaScript 物件的變更
- 新增、變更、刪除屬性時
- 陣列中的元素組合或退出時
- 物件的原型變更
資料繫結的重要性
當您重視模型檢視畫面控制項的分離,資料繫結就會開始。HTML 是良好的宣告式機制,但是完全靜態的。在理想情況下,您只需要宣告資料和 DOM 之間的關聯性,讓 DOM 保持在最新狀態。這麼做可以節省您許多時間編寫真正重複的程式碼,只需在應用程式內部狀態或伺服器之間,將資料傳送至 DOM,或從 DOM 接收資料即可。
假如您擁有複雜的使用者介面,需要在資料模型中連接多個屬性與檢視區塊中的多個元素之間的關聯時,就特別適合使用資料繫結。在我們建構的單頁應用程式中,這是很常見的情況。
我們開發了一種原生在瀏覽器中觀察資料的方式,因此,我們讓 JavaScript 架構 (以及您撰寫的小型公用程式庫) 能夠觀察模型資料的變化,而不必依賴現今世界上一些緩慢的入侵手段。
現今世界的樣貌
日常檢查
您之前在哪裡看過資料繫結?如果您使用新型 MV* 程式庫來建構網頁應用程式 (例如 Angular、Knockout),您可能已習慣將模型資料繫結至 DOM。以下提供「電話清單」應用程式範例,其中我們將 phones
陣列 (在 JavaScript 中定義) 中每支手機的值繫結至清單項目,讓我們的資料和使用者介面始終保持同步:
<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()
我們理想的情況下是兩方的最佳選擇:如果我們選擇使用 AND 且不需每次都進行錯誤檢查,可以透過此方式觀察資料並支援原始資料物件 (一般 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;
如我們所見,傳回的變更報表中包含刪除作業的相關資訊。和預期一樣,屬性的新值現在為未定義狀態。現在,我們就可以知道已新增屬性的時間。刪除時間。基本上,物件屬性的「set」(「new」、「deleted」和「reConfigure」),以及用於變更原型 (proto)。
與任何觀察系統中一樣,方法也是一種停止監聽變更的方法。在此範例中,Object.unobserve()
擁有與 O.o() 相同的簽章,但可按照以下方式呼叫:
Object.unobserve(todoModel, observer);
如下所示,此函式執行後,如果對物件進行任何更動,就不會再產生變更記錄清單。
指定感興趣的變更
因此,我們探討瞭如何取回觀察物件變更清單的基本概念。如果您只想查看物件的部分變更,而不是所有變更,該怎麼辦?所有人都需要垃圾郵件篩選器。也就是說,觀察器只能指定接受清單,以便指定想聽取的變更類型。您可以使用第三個 O.o() 引數來指定 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);
當資料屬性值有所變更 (「update」) 時,系統就會回報。物件實作選擇回報的任何其他項目 (notifier.notifyChange()
)。
在網路平台上擁有多年經驗後,我們瞭解同步做法就是您的首要之務,因為作業方式最簡單,問題是建立在本質上具有危險性的處理模型。如果您正在編寫程式碼,然後更新物件的屬性,您一定會希望更新該物件的屬性是否會邀請任意程式碼來執行想要的任何動作。對在函數中間執行時,您的假設並不理想。
如果你是觀察器,建議不要有人正在對話。你不會想要工作在世界上相互不一致的狀況下。最終會進行更多錯誤檢查。試圖容許更多不好的情況,通常也是難以工作的模型。非同步較難處理,但最終更好的模型在一天結束時會更好。
解決這個問題的方法為綜合變更記錄。
綜合變更記錄
基本上,如果您想擁有存取子或已運算的屬性,就必須在這些值變更時通知您。這只是需要一點額外工作,但它的設計是這項機制的一流功能,而且這些通知會與基礎資料物件的其餘通知一起傳送。來自資料屬性。
觀察存取子和計算的屬性可以使用 notifier.notify 解決為 O.o() 的另一個部分。大部分的觀察系統都希望透過某種形式觀察衍生值。方法有很多種,O.o 不認同「正確」標準。運算屬性必須是在內部 (私人) 狀態變更時notify的存取子。
再次提醒您,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() 的另一個可行模式是單一回呼觀察器的概念。這可將單一回呼做為許多不同物件的「觀察器」使用。在「微任務結束時」,回呼將會向其觀察的所有物件傳送完整的一組變更 (請注意與 Mutation Observers 相似)。
大規模變更
也許您正在開發規模龐大的應用程式,且經常需要處理大規模的變更。物件或許會想說明較大的語意變更,以更精簡的方式影響許多屬性 (而非廣播大量屬性變更)。
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 函式」中的所有內容,都算是「大幅變化」。接受「大幅調整」的觀察者只會收到「大幅變動」記錄。觀察器不會收到因為「執行函式」工作而產生的基礎變化。
觀察陣列
我們之前說過觀察物件變動,應該是關於陣列呢?好問題。有人告訴我:「好問題」。我很忙著自信地想問這麼一個好問題,所以沒聽到他們回答的,不過我有詳細說明。我們也提供陣列使用的新方法!
Array.observe()
是一種將大規模變更視為自身的方法 (例如,接合、未偏移或任何會隱含變更長度的任何項目),作為「剪接」變更記錄。內部使用 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() 視為讀取快取。一般來說,在下列情況中,快取會是不錯的選擇 (按重要性排序):
- 讀取頻率會影響寫入頻率。
- 您可以建立一個快取,在寫入期間投入的穩定工作量進行交易,讓演算法在讀取期間獲得更好的效能。
- 可以在接受寫入時不斷減慢。
O.o() 是專為如 1 的用途而設計。
需要為觀察的所有資料保留副本,再進行骯髒檢查。這意味著您只使用 O.o() 就對結構進行記憶體檢查,卻會使您因執行 O.o() 而需進行髒汙檢查。Drty 檢查,雖然沒有明確的停靠點解決方案,實際上卻是根本洩漏的抽象化機制,可能會為應用程式造成不必要的複雜性。
原因何在?也就是說,在資料「可能」出現變化時,就必須執行這些項目。然而,並沒有一種確實能做到這一點,且任何執行的方法都有明顯的缺點 (例如,檢查輪詢間隔可能會產生視覺構件,以及程式碼問題之間的競爭狀況)。消化檢查也需要部署觀察器的全域登錄,造成記憶體流失的危害,並降低成本 O.o()。
讓我們來看看一些數據。
下列基準測試 (可在 GitHub 取得) 用來比較骯髒檢查和 O.o() 的結構,它們的結構為觀察 ed-Object-Set-Size 與 Mutations 的圖形。一般的結果是,進行骯髒檢查的效能,會透過演算法與觀察到的物件數量成正比,O.o() 效能則與異動數量成正比。
日常檢查
已啟用 Object.observe() 的 Chrome
聚填 Object.observe()
很好!在 Chrome 36 版中可以使用 O.o(),但在其他瀏覽器中使用 O.o() 如何呢?請放心,我們會提供協助。Polymer 的 Observe-JS 是 O.o() 的 polyfill (如果有的話),會使用原生實作,但在其他方面執行 polyfill,並包含實用的糖果。而是呈現統整全球各種變動情況,並提供各項變化的報告。它公開有兩個非常強大的功能:
- 您可以觀察路徑。這表示我想觀察指定物件中的「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.
});
- 會說明陣列切片。陣列剪接中基本上,為將舊版陣列轉換成新版陣列,您必須對陣列執行最小的 S 接合作業組合。這是一種轉換類型,或是陣列的不同檢視畫面。也就是從舊狀態移至新狀態至少需要執行的工作量。
以最小配量組合回報陣列變更的範例:
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 的變更偵測功能撰寫了一份設計文件,他們日後可以在 Chrome 穩定版中使用 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 發表意見與評論。