Observables
Knockout是在下面三個(gè)核心功能是建立起來(lái)的:
?這一節(jié),你講學(xué)到3個(gè)功能中的第一個(gè)。 在這之前, 我們來(lái)解釋一下MVVM模式和view model的概念。
?MVVM and View Models
Model-View-View Model (MVVM) 是一種創(chuàng)建用戶界面的設(shè)計(jì)模式。 描述的是如何將復(fù)雜的UI用戶界面分成3個(gè)部分:
注意這不是UI本身:它不包含任何按鈕的概念或者顯示風(fēng)格。它也不是持續(xù)數(shù)據(jù)模型 – 包含用戶正在使用的未保存數(shù)據(jù)。使用KO的時(shí)候,你的view models是不包含任何HTML知識(shí)的純JavaScript 對(duì)象。保持view model抽象可以保持簡(jiǎn)單,以便你能管理更復(fù)雜的行為。
使用KO的時(shí)候,你的view就是你帶有綁定信息的HTML文檔,這些聲明式的綁定管理到你的view model上?;蛘吣憧梢允褂媚0鍙哪愕膙iew model獲取數(shù)據(jù)生成HTML。
創(chuàng)建一個(gè)view model,只需要聲明任意的JavaScript object。例如:
var myViewModel = {
personName: 'Bob',
personAge: 123
};
你可以為view model創(chuàng)建一個(gè)聲明式綁定的簡(jiǎn)單view。例如:下面的代碼顯示personName 值:
The name is <span data-bind="text: personName"></span>
Activating Knockout
data-bind屬性盡快好用但它不是HTML的原生屬性(它嚴(yán)格遵從HTML5語(yǔ)法, 雖然HTML4驗(yàn)證器提示有不可識(shí)別的屬性但依然可用)。由于瀏覽器不識(shí)別它是什么意思,所以你需要激活Knockout 來(lái)讓他起作用。
激活Knockout,需要添加如下的 代碼塊:
ko.applyBindings(myViewModel);
你可以將這個(gè)代碼塊放在HTML底部,或者放在jQuery的$函數(shù)或者ready 函數(shù)里,然后放在頁(yè)面上面, 最終生成結(jié)果就是如下的HTML代碼:
The name is <span>Bob</span>
你可能奇怪ko.applyBindings使用的是什么樣的參數(shù),
Observables
現(xiàn)在已經(jīng)知道如何創(chuàng)建一個(gè)簡(jiǎn)單的view model并且通過binding顯示它的屬性了。但是KO一個(gè)重要的功能是當(dāng)你的view model改變的時(shí)候能自動(dòng)更新你的界面。當(dāng)你的view model部分改變的時(shí)候KO是如何知道的呢?答案是:你需要將你的model屬性聲明成observable的, 因?yàn)樗欠浅L厥獾腏avaScript objects,能夠通知訂閱者它的改變以及自動(dòng)探測(cè)到相關(guān)的依賴。
例如:將上述例子的view model改成如下代碼:
var myViewModel = {
personName: ko.observable('Bob'),
personAge: ko.observable(123)
};
你根本不需要修改view – 所有的data-bind語(yǔ)法依然工作,不同的是他能監(jiān)控到變化,當(dāng)值改變時(shí),view會(huì)自動(dòng)更新。
監(jiān)控屬性(observables)的讀和寫
不是所有的瀏覽器都支持JavaScript的 getters and setters (比如IE),,所以為了兼容性,使用ko.observable監(jiān)控的對(duì)象都是真實(shí)的function函數(shù)。
監(jiān)控屬性(observables)的特征就是監(jiān)控(observed),例如其它代碼可以說(shuō)我需要得到對(duì)象變化的通知,所以KO內(nèi)部有很多內(nèi)置的綁定語(yǔ)法。所以如果你的代碼寫成data-bind="text: personName", text綁定注冊(cè)到自身,一旦personName的值改變,它就能得到通知。
當(dāng)然調(diào)用myViewModel.personName('Mary')改變name的值,text綁定將自動(dòng)更新這個(gè)新值到相應(yīng)的DOM元素上。這就是如何將view model的改變傳播到view上的。
監(jiān)控屬性(Observables)的顯式訂閱
通常情況下,你不用手工訂閱,所以新手可以忽略此小節(jié)。高級(jí)用戶,如果你要注冊(cè)自己的訂閱到監(jiān)控屬性(observables),你可以調(diào)用它的subscribe 函數(shù)。例如:
myViewModel.personName.subscribe(function (newValue) {
alert("The person's new name is " + newValue);
});
這個(gè)subscribe 函數(shù)在內(nèi)部很多地方都用到的。你也可以終止自己的訂閱:首先得到你的訂閱,然后調(diào)用這個(gè)對(duì)象的dispose函數(shù),例如:
var subscription = myViewModel.personName.subscribe(function (newValue) { /* do stuff */ });
// ...then later...
subscription.dispose(); // I no longer want notifications
大多數(shù)情況下,你不需要做這些,因?yàn)閮?nèi)置的綁定和模板系統(tǒng)已經(jīng)幫你做好很多事情了,可以直接使用它們。
如果你已經(jīng)有了監(jiān)控屬性firstName和lastName,你想顯示全稱怎么辦? 這就需要用到依賴監(jiān)控屬性了 – 這些函數(shù)是一個(gè)或多個(gè)監(jiān)控屬性, 如果他們的依賴對(duì)象改變,他們會(huì)自動(dòng)跟著改變。
例如,下面的view model,
var viewModel = {
firstName: ko.observable('Bob'),
lastName: ko.observable('Smith')
};
… 你可以添加一個(gè)依賴監(jiān)控屬性來(lái)返回姓名全稱:
viewModel.fullName = ko.dependentObservable(function () {
return this.firstName() + " " + this.lastName();
}, viewModel);
并且綁定到UI的元素上,例如:
The name is <span data-bind="text: fullName"></span>
… 不管firstName還是lastName改變,全稱fullName都會(huì)自動(dòng)更新(不管誰(shuí)改變,執(zhí)行函數(shù)都會(huì)調(diào)用一次,不管改變成什么,他的值都會(huì)更新到UI或者其他依賴監(jiān)控屬性上)。
管理‘this’
新手可忽略此小節(jié),你只需要安裝上面例子中的代碼模式寫就行了,無(wú)需知道/關(guān)注這個(gè)this。
你可能疑惑ko.dependentObservable的第二個(gè)參數(shù)是做什么用的(上面的例子中我傳的是viewModel), 它是聲明執(zhí)行依賴監(jiān)控屬性的this用的。 沒有它,你不能引用到this.firstName() 和this.lastName()。 老練的JavaScript 開發(fā)人員不覺得this怎么樣,但是如果你不熟悉JavaScript,那就對(duì)它就會(huì)很陌生。(C#和Java需要不需要為set一個(gè)值為設(shè)置this,但是JavaScript 需要,因?yàn)槟J(rèn)情況下他們的函數(shù)自身不是任何對(duì)象的一部分)。
不幸的是, JavaScript 對(duì)象沒有任何辦法能引用他們自身,所以你需要通過myViewModelObject.myDependentObservable = ... 的形式添加依賴監(jiān)控屬性到view model對(duì)象上。 你不能直接在view model里聲明他們,換句話說(shuō),你不能寫成下面這樣:
var viewModel = {
myDependentObservable: ko.dependentObservable(function() {
...
}, /* can't refer to viewModel from here, so this doesn't work */)
}
… 相反你必須寫成如下這樣:
var viewModel = {
// Add other properties here as you wish
};
viewModel.myDependentObservable = ko.dependentObservable(function() {
...
}, viewModel); // This is OK
只要你知道期望什么,它確實(shí)不是個(gè)問題。J
依賴鏈
理所當(dāng)然,如果你想你可以創(chuàng)建一個(gè)依賴監(jiān)控屬性的鏈。例如:
?然后,items或者selectedIndexes 的改變將會(huì)影響到所有依賴監(jiān)控屬性的鏈,所有綁定這些屬性的UI元素都會(huì)自動(dòng)更新。多么整齊與優(yōu)雅!
可寫的依賴監(jiān)控屬性
新手可忽略此小節(jié),可寫依賴監(jiān)控屬性真的是太advanced了,而且大部分情況下都用不到。
正如所學(xué)到的,依賴監(jiān)控屬性是通過計(jì)算其它的監(jiān)控屬性而得到的。感覺是依賴監(jiān)控屬性正常情況下應(yīng)該是只讀的。那么,有可能讓依賴監(jiān)控屬性支持可寫么?你只需要聲明自己的callback函數(shù)然后利用寫入的值再處理一下相應(yīng)的邏輯即可。
你可以像使用普通的監(jiān)控屬性一樣使用依賴監(jiān)控屬性 – 數(shù)據(jù)雙向綁定到DOM元素上,并且通過自定義的邏輯攔截所有的讀和寫操作。這是非常牛逼的特性并且可以在大范圍內(nèi)使用。
例1:分解用戶的輸入
返回到經(jīng)典的“first name + last name = full name” 例子上,你可以讓事情調(diào)回來(lái)看: 讓依賴監(jiān)控屬性fullName可寫,讓用戶直接輸入姓名全稱,然后輸入的值將被解析并映射寫入到基本的監(jiān)控屬性firstName和lastName上:
var viewModel = {
firstName: ko.observable("Planet"),
lastName: ko.observable("Earth")
};
viewModel.fullName = ko.dependentObservable({
read: function () {
return this.firstName() + " " + this.lastName();
},
write: function (value) {
var lastSpacePos = value.lastIndexOf(" ");
if (lastSpacePos > 0) { // Ignore values with no space character
this.firstName(value.substring(0, lastSpacePos)); // Update "firstName"
this.lastName(value.substring(lastSpacePos + 1)); // Update "lastName"
}
},
owner: viewModel
});
這個(gè)例子里,寫操作的callback接受寫入的值,把值分離出來(lái),分別寫入到“firstName”和“l(fā)astName”上。 你可以像普通情況一樣將這個(gè)view model綁定到DOM元素上,如下:
<p>First name: <span data-bind="text: firstName"></span></p>
<p>Last name: <span data-bind="text: lastName"></span></p>
<h2>Hello, <input data-bind="value: fullName"/>!</h2>
這是一個(gè)Hello World 例子的反例子,姓和名都不可編輯,相反姓和名組成的姓名全稱卻是可編輯的。
上面的view model演示的是通過一個(gè)簡(jiǎn)單的參數(shù)來(lái)初始化依賴監(jiān)控屬性。你可以給下面的屬性傳入任何JavaScript對(duì)象:
?例2:Value轉(zhuǎn)換器
?有時(shí)候你可能需要顯示一些不同格式的數(shù)據(jù),從基礎(chǔ)的數(shù)據(jù)轉(zhuǎn)化成顯示格式。比如,你存儲(chǔ)價(jià)格為float類型,但是允許用戶編輯的字段需要支持貨幣單位和小數(shù)點(diǎn)。你可以用可寫的依賴監(jiān)控屬性來(lái)實(shí)現(xiàn),然后解析傳入的數(shù)據(jù)到基本 float類型里:
viewModel.formattedPrice = ko.dependentObservable({
read: function () {
return "$" + this.price().toFixed(2);
},
write: function (value) {
// Strip out unwanted characters, parse as float, then write the raw data back to the underlying "price" observable
value = parseFloat(value.replace(/[^\.\d]/g, ""));
this.price(isNaN(value) ? 0 : value); // Write to underlying storage
},
owner: viewModel
});
然后我們綁定formattedPrice到text box上:
<p>Enter bid price: <input data-bind="value: formattedPrice"/></p>
所以,不管用戶什么時(shí)候輸入新價(jià)格,輸入什么格式,text box里會(huì)自動(dòng)更新為帶有2位小數(shù)點(diǎn)和貨幣符號(hào)的數(shù)值。這樣用戶可以看到你的程序有多聰明,來(lái)告訴用戶只能輸入2位小數(shù),否則的話自動(dòng)刪除多余的位數(shù),當(dāng)然也不能輸入負(fù)數(shù),因?yàn)閣rite的callback函數(shù)會(huì)自動(dòng)刪除負(fù)號(hào)。
例3:過濾并驗(yàn)證用戶輸入
例1展示的是寫操作過濾的功能,如果你寫的值不符合條件的話將不會(huì)被寫入,忽略所有不包括空格的值。
再多走一步,你可以聲明一個(gè)監(jiān)控屬性isValid 來(lái)表示最后一次寫入是否合法,然后根據(jù)真假值顯示相應(yīng)的提示信息。稍后仔細(xì)介紹,先參考如下代碼:
var viewModel = {
acceptedNumericValue: ko.observable(123),
lastInputWasValid: ko.observable(true)
};
viewModel.attemptedValue = ko.dependentObservable({
read: viewModel.acceptedNumericValue,
write: function (value) {
if (isNaN(value))
this.lastInputWasValid(false);
else {
this.lastInputWasValid(true);
this.acceptedNumericValue(value); // Write to underlying storage
}
},
owner: viewModel
});
… 按照如下格式聲明綁定元素:
<p>Enter a numeric value: <input data-bind="value: attemptedValue"/></p>
<div data-bind="visible: !lastInputWasValid()">That's not a number!</div>
現(xiàn)在,acceptedNumericValue 將只接受數(shù)字,其它任何輸入的值都會(huì)觸發(fā)顯示驗(yàn)證信息,而會(huì)更新acceptedNumericValue。
備注:上面的例子顯得殺傷力太強(qiáng)了,更簡(jiǎn)單的方式是在上使用jQuery Validation和number class。Knockout可以和jQuery Validation一起很好的使用,參考例子:grid editor?。當(dāng)然,上面的例子依然展示了一個(gè)如何使用自定義邏輯進(jìn)行過濾和驗(yàn)證數(shù)據(jù),如果驗(yàn)證很復(fù)雜而jQuery Validation很難使用的話,你就可以用它。
依賴跟蹤如何工作的
新手沒必要知道太清楚,但是高級(jí)開發(fā)人員可以需要知道為什么依賴監(jiān)控屬性能夠自動(dòng)跟蹤并且自動(dòng)更新UI…
事實(shí)上,非常簡(jiǎn)單,甚至說(shuō)可愛。跟蹤的邏輯是這樣的:
所有說(shuō),KO不僅僅是在第一次執(zhí)行函數(shù)執(zhí)行時(shí)候探測(cè)你的依賴項(xiàng),每次它都會(huì)探測(cè)。舉例來(lái)說(shuō),你的依賴屬性可以是動(dòng)態(tài)的:依賴屬性A代表你是否依賴于依賴屬性B或者C,這時(shí)候只有當(dāng)A或者你當(dāng)前的選擇B或者C改變的時(shí)候執(zhí)行函數(shù)才重新執(zhí)行。你不需要再聲明其它的依賴:運(yùn)行時(shí)會(huì)自動(dòng)探測(cè)到的。
另外一個(gè)技巧是:一個(gè)模板輸出的綁定是依賴監(jiān)控屬性的簡(jiǎn)單實(shí)現(xiàn),如果模板讀取一個(gè)監(jiān)控屬性的值,那模板綁定就會(huì)自動(dòng)變成依賴監(jiān)控屬性依賴于那個(gè)監(jiān)控屬性,監(jiān)控屬性一旦改變,模板綁定的依賴監(jiān)控屬性就會(huì)自動(dòng)執(zhí)行。嵌套的模板也是自動(dòng)的:如果模板X render模板 Y,并且Y需要顯示監(jiān)控屬性Z的值,當(dāng)Z改變的時(shí)候,由于只有Y依賴它,所以只有Y這部分進(jìn)行了重新繪制(render)。
如果你要探測(cè)和響應(yīng)一個(gè)對(duì)象的變化,你應(yīng)該用observables。如果你需要探測(cè)和響應(yīng)一個(gè)集合對(duì)象的變化,你應(yīng)該用observableArray 。在很多場(chǎng)景下,它都非常有用,比如你要在UI上需要顯示/編輯的一個(gè)列表數(shù)據(jù)集合,然后對(duì)集合進(jìn)行添加和刪除。
var myObservableArray = ko.observableArray(); // Initially an empty array
myObservableArray.push('Some value'); // Adds the value and notifies observers
關(guān)鍵點(diǎn):監(jiān)控?cái)?shù)組跟蹤的是數(shù)組里的對(duì)象,而不是這些對(duì)象自身的狀態(tài)。
簡(jiǎn)單說(shuō),將一對(duì)象放在observableArray 里不會(huì)使這個(gè)對(duì)象本身的屬性變化可監(jiān)控的。當(dāng)然你自己也可以聲明這個(gè)對(duì)象的屬性為observable的,但它就成了一個(gè)依賴監(jiān)控對(duì)象了。一個(gè)observableArray 僅僅監(jiān)控他擁有的對(duì)象,并在這些對(duì)象添加或者刪除的時(shí)候發(fā)出通知。
預(yù)加載一個(gè)監(jiān)控?cái)?shù)組observableArray
如果你想讓你的監(jiān)控?cái)?shù)組在開始的時(shí)候就有一些初始值,那么在聲明的時(shí)候,你可以在構(gòu)造器里加入這些初始對(duì)象。例如:
// This observable array initially contains three objects
var anotherObservableArray = ko.observableArray([
{ name: "Bungle", type: "Bear" },
{ name: "George", type: "Hippo" },
{ name: "Zippy", type: "Unknown" }
]);
從observableArray里讀取信息
一個(gè)observableArray其實(shí)就是一個(gè)observable的監(jiān)控對(duì)象,只不過他的值是一個(gè)數(shù)組(observableArray還加了很多其他特性,稍后介紹)。所以你可以像獲取普通的observable的值一樣,只需要調(diào)用無(wú)參函數(shù)就可以獲取自身的值了。 例如,你可以像下面這樣獲取它的值:
alert('The length of the array is ' + myObservableArray().length);
alert('The first element is ' + myObservableArray()[0]);
理論上你可以使用任何原生的JavaScript數(shù)組函數(shù)來(lái)操作這些數(shù)組,但是KO提供了更好的功能等價(jià)函數(shù),他們非常有用是因?yàn)椋?/p>
下面講解的均是observableArray的讀取和寫入的相關(guān)函數(shù)。
indexOf
indexOf 函數(shù)返回的是第一個(gè)等于你參數(shù)數(shù)組項(xiàng)的索引。例如:myObservableArray.indexOf('Blah')將返回以0為第一個(gè)索引的第一個(gè)等于Blah的數(shù)組項(xiàng)的索引。如果沒有找到相等的,將返回-1。
slice
slice函數(shù)是observableArray相對(duì)于JavaScript 原生函數(shù)slice的等價(jià)函數(shù)(返回給定的從開始索引到結(jié)束索引之間所有的對(duì)象集合)。 調(diào)用myObservableArray.slice(...)等價(jià)于調(diào)用JavaScript原生函數(shù)(例如:myObservableArray().slice(...))。
操作observableArray
observableArray 展現(xiàn)的是數(shù)組對(duì)象相似的函數(shù)并通知訂閱者的功能。
pop, push, shift, unshift, reverse, sort, splice
所有這些函數(shù)都是和JavaScript數(shù)組原生函數(shù)等價(jià)的,唯一不同的數(shù)組改變可以通知訂閱者:
????myObservableArray.push('Some new value')?在數(shù)組末尾添加一個(gè)新項(xiàng)
????myObservableArray.pop()?刪除數(shù)組最后一個(gè)項(xiàng)并返回該項(xiàng)
????myObservableArray.unshift('Some new value')?在數(shù)組頭部添加一個(gè)項(xiàng)
????myObservableArray.shift()?刪除數(shù)組頭部第一項(xiàng)并返回該項(xiàng)
????myObservableArray.reverse()?翻轉(zhuǎn)整個(gè)數(shù)組的順序
????myObservableArray.sort()?給數(shù)組排序
??????? 默認(rèn)情況下,是按照字符排序(如果是字符)或者數(shù)字排序(如果是數(shù)字)。
??????? 你可以排序傳入一個(gè)排序函數(shù)進(jìn)行排序,該排序函數(shù)需要接受2個(gè)參數(shù)(代表該數(shù)組里需要比較的項(xiàng)),如果第一個(gè)項(xiàng)小于第二個(gè)項(xiàng),返回-1,大于則返回1,等于返回0。例如:用lastname給person排序,你可以這樣寫:myObservableArray.sort (function (left, right) {return left.lastName == right.lastName? 0: (left.lastName
?????myObservableArray.splice()?刪除指定開始索引和指定數(shù)目的數(shù)組對(duì)象元素。例如myObservableArray.splice(1, 3)?從索引1開始刪除3個(gè)元素(第2,3,4個(gè)元素)然后將這些元素作為一個(gè)數(shù)組對(duì)象返回。
更多observableArray 函數(shù)的信息,請(qǐng)參考等價(jià)的JavaScript數(shù)組標(biāo)準(zhǔn)函數(shù)。
**remove**和removeAll
observableArray 添加了一些JavaScript數(shù)組默認(rèn)沒有但非常有用的函數(shù):
????myObservableArray.remove(someItem)?刪除所有等于someItem的元素并將被刪除元素作為一個(gè)數(shù)組返回
????myObservableArray.remove(function(item) { return item.age ?刪除所有age屬性小于18的元素并將被刪除元素作為一個(gè)數(shù)組返回
????myObservableArray.removeAll(['Chad', 132, undefined])?刪除所有等于'Chad', 123, or undefined的元素并將被刪除元素作為一個(gè)數(shù)組返回
**destroy**和destroyAll(注:通常只和和Ruby on Rails開發(fā)者有關(guān))
destroy和destroyAll函數(shù)是為Ruby on Rails開發(fā)者方便使用為開發(fā)的:
????myObservableArray.destroy(someItem)?找出所有等于someItem的元素并給他們添加一個(gè)屬性_destroy,并賦值為true
????myObservableArray.destroy(function(someItem) { return someItem.age ?找出所有age屬性小于18的元素并給他們添加一個(gè)屬性_destroy,并賦值為true
????myObservableArray.destroyAll(['Chad', 132, undefined])?找出所有等于'Chad', 123, 或undefined 的元素并給他們添加一個(gè)屬性_destroy,并賦值為true
那么,_destroy是做什么用的?正如我提到的,這只是為Rails 開發(fā)者準(zhǔn)備的。在Rails 開發(fā)過程中,如果你傳入一個(gè)JSON對(duì)象,Rails 框架會(huì)自動(dòng)轉(zhuǎn)換成ActiveRecord對(duì)象并且保存到數(shù)據(jù)庫(kù)。Rails 框架知道哪些對(duì)象以及在數(shù)據(jù)庫(kù)中存在,哪些需要添加或更新, 標(biāo)記_destroy為true就是告訴框架刪除這條記錄。
注意的是:在KO render一個(gè)foreach模板的時(shí)候,會(huì)自動(dòng)隱藏帶有_destroy屬性并且值為true的元素。所以如果你的“delete”按鈕調(diào)用destroy(someItem)?方法的話,UI界面上的相對(duì)應(yīng)的元素將自動(dòng)隱藏,然后等你提交這個(gè)JSON對(duì)象到Rails上的時(shí)候,這個(gè)元素項(xiàng)將從數(shù)據(jù)庫(kù)刪除(同時(shí)其它的元素項(xiàng)將正常的插入或者更新)。
更多建議: