天天看點

Knockout應用開發指南 第二章:監控屬性(Observables)

關于Knockout的3個重要概念(Observables,DependentObservables,ObservableArray),本人無法準确表達它的準确含義,是以暫定翻譯為(監控屬性、依賴監控屬性和監控數組),如果有好的建議請指正,多謝。

Observables

Knockout是在下面三個核心功能是建立起來的:

監控屬性(Observables)和依賴跟蹤(Dependency tracking)

聲明式綁定(Declarative bindings)

模闆(Templating)

 這一節,你講學到3個功能中的第一個。 在這之前, 我們來解釋一下MVVM模式和view model的概念。

 MVVM and View Models

Model-View-View Model (MVVM) 是一種建立使用者界面的設計模式。 描述的是如何将複雜的UI使用者界面分成3個部分:

 model: 你程式裡存儲的資料。這個資料包括對象和業務操作(例如:銀子賬戶可以完成轉賬功能), 并且獨立于任何UI。使用KO的時候,通常說是向伺服器調用Ajax讀寫這個存儲的模型資料。

 view model: 在UI上,純code描述的資料以及操作。例如,如果你實作清單編輯,你的view model應該是一個包含清單項items的對象和暴露的add/remove清單項(item)的操作方法。

    注意這不是UI本身:它不包含任何按鈕的概念或者顯示風格。它也不是持續資料模型 – 包含使用者正在使用的未儲存資料。使用KO的時候,你的view models是不包含任何HTML知識的純JavaScript 對象。保持view model抽象可以保持簡單,以便你能管理更複雜的行為。

 view: 一個可見的,互動式的,表示view model狀态的UI。 從view model顯示資料,發送指令到view model(例如:當使用者click按鈕的時候) ,任何view model狀态改變的時候更新。

使用KO的時候,你的view就是你帶有綁定資訊的HTML文檔,這些聲明式的綁定管理到你的view model上。或者你可以使用模闆從你的view model擷取資料生成HTML。

建立一個view model,隻需要聲明任意的JavaScript object。例如:

你可以為view model建立一個聲明式綁定的簡單view。例如:下面的代碼顯示personName 值:

Activating Knockout

data-bind屬性盡快好用但它不是HTML的原生屬性(它嚴格遵從HTML5文法, 雖然HTML4驗證器提示有不可識别的屬性但依然可用)。由于浏覽器不識别它是什麼意思,是以你需要激活Knockout 來讓他起作用。

激活Knockout,需要添加如下的 <script> 代碼塊:

你可以将這個代碼塊放在HTML底部,或者放在jQuery的$函數或者ready 函數裡,然後放在頁面上面, 最終生成結果就是如下的HTML代碼:

你可能奇怪ko.applyBindings使用的是什麼樣的參數,

 第一個參數是你想用于聲明式綁定

第二個參數(可選),可以聲明成使用data-bind的HTML元素或者容器。例如, ko.applyBindings(myViewModel, document.getElementById('someElementId'))。它的現在是隻有作為someElementId 的元素和子元素才能激活KO功能。 好處是你可以在同一個頁面聲明多個view model,用來區分區域。

現在已經知道如何建立一個簡單的view model并且通過binding顯示它的屬性了。但是KO一個重要的功能是當你的view model改變的時候能自動更新你的界面。當你的view model部分改變的時候KO是如何知道的呢?答案是:你需要将你的model屬性聲明成observable的, 因為它是非常特殊的JavaScript objects,能夠通知訂閱者它的改變以及自動探測到相關的依賴。

例如:将上述例子的view model改成如下代碼:

你根本不需要修改view – 所有的data-bind文法依然工作,不同的是他能監控到變化,當值改變時,view會自動更新。

監控屬性(observables)的讀和寫

不是所有的浏覽器都支援JavaScript的 getters and setters (比如IE),,是以為了相容性,使用ko.observable監控的對象都是真實的function函數。

 讀取監控屬性(observable)的值,隻需要直接調用監控屬性(observable)(不需要參數),例如myViewModel.personName() 将傳回'Bob', myViewModel.personAge() 将傳回 123。

寫一個新值到監控屬性(observable)上,調用這個observable屬性并當新值作為參數。例如:調用 myViewModel.personName('Mary') 将更新name值為'Mary'。

給一個model對象的多個屬性寫入新值,你可以使用鍊式文法。例如: myViewModel.personName('Mary').personAge(50) 将會将name更新為 'Mary' 并且 将age更新為 50.

監控屬性(observables)的特征就是監控(observed),例如其它代碼可以說我需要得到對象變化的通知,是以KO内部有很多内置的綁定文法。是以如果你的代碼寫成data-bind="text: personName", text綁定注冊到自身,一旦personName的值改變,它就能得到通知。

當然調用myViewModel.personName('Mary')改變name的值,text綁定将自動更新這個新值到相應的DOM元素上。這就是如何将view model的改變傳播到view上的。

監控屬性(Observables)的顯式訂閱

通常情況下,你不用手工訂閱,是以新手可以忽略此小節。進階使用者,如果你要注冊自己的訂閱到監控屬性(observables),你可以調用它的subscribe 函數。例如:

這個subscribe 函數在内部很多地方都用到的。你也可以終止自己的訂閱:首先得到你的訂閱,然後調用這個對象的dispose函數,例如:

大多數情況下,你不需要做這些,因為内置的綁定和模闆系統已經幫你做好很多事情了,可以直接使用它們。

如果你已經有了監控屬性firstName和lastName,你想顯示全稱怎麼辦? 這就需要用到依賴監控屬性了 – 這些函數是一個或多個監控屬性, 如果他們的依賴對象改變,他們會自動跟着改變。

例如,下面的view model,

… 你可以添加一個依賴監控屬性來傳回姓名全稱:

并且綁定到UI的元素上,例如:

… 不管firstName還是lastName改變,全稱fullName都會自動更新(不管誰改變,執行函數都會調用一次,不管改變成什麼,他的值都會更新到UI或者其他依賴監控屬性上)。

管理‘this’

新手可忽略此小節,你隻需要安裝上面例子中的代碼模式寫就行了,無需知道/關注這個this。

你可能疑惑ko.dependentObservable的第二個參數是做什麼用的(上面的例子中我傳的是viewModel), 它是聲明執行依賴監控屬性的this用的。 沒有它,你不能引用到this.firstName() 和this.lastName()。 老練的JavaScript 開發人員不覺得this怎麼樣,但是如果你不熟悉JavaScript,那就對它就會很陌生。(C#和Java需要不需要為set一個值為設定this,但是JavaScript 需要,因為預設情況下他們的函數自身不是任何對象的一部分)。

不幸的是, JavaScript 對象沒有任何辦法能引用他們自身,是以你需要通過myViewModelObject.myDependentObservable = ... 的形式添加依賴監控屬性到view model對象上。 你不能直接在view model裡聲明他們,換句話說,你不能寫成下面這樣:

… 相反你必須寫成如下這樣:

隻要你知道期望什麼,它确實不是個問題。J

依賴鍊

理所當然,如果你想你可以建立一個依賴監控屬性的鍊。例如:

監控屬性items表述一組清單項

監控屬性selectedIndexes儲存着被使用者選上的清單項的索引

依賴監控屬性selectedItems 傳回的是selectedIndexes 對應的清單項數組

另一個依賴監控屬性傳回的true或false依賴于 selectedItems 的各個清單項是否包含一些屬性(例如,是否新的或者還未儲存的)。一些UI element(像按鈕的啟用/禁用)的狀态取決于這個值)。

 然後,items或者selectedIndexes 的改變将會影響到所有依賴監控屬性的鍊,所有綁定這些屬性的UI元素都會自動更新。多麼整齊與優雅!

可寫的依賴監控屬性

新手可忽略此小節,可寫依賴監控屬性真的是太advanced了,而且大部分情況下都用不到。

正如所學到的,依賴監控屬性是通過計算其它的監控屬性而得到的。感覺是依賴監控屬性正常情況下應該是隻讀的。那麼,有可能讓依賴監控屬性支援可寫麼?你隻需要聲明自己的callback函數然後利用寫入的值再處理一下相應的邏輯即可。

你可以像使用普通的監控屬性一樣使用依賴監控屬性 – 資料雙向綁定到DOM元素上,并且通過自定義的邏輯攔截所有的讀和寫操作。這是非常牛逼的特性并且可以在大範圍内使用。

例1:分解使用者的輸入

傳回到經典的“first name + last name = full name” 例子上,你可以讓事情調回來看: 讓依賴監控屬性fullName可寫,讓使用者直接輸入姓名全稱,然後輸入的值将被解析并映射寫入到基本的監控屬性firstName和lastName上:

這個例子裡,寫操作的callback接受寫入的值,把值分離出來,分别寫入到“firstName”和“lastName”上。 你可以像普通情況一樣将這個view model綁定到DOM元素上,如下:

這是一個Hello World 例子的反例子,姓和名都不可編輯,相反姓和名組成的姓名全稱卻是可編輯的。

上面的view model示範的是通過一個簡單的參數來初始化依賴監控屬性。你可以給下面的屬性傳入任何JavaScript對象:

   read — 必選,一個用來執行取得依賴監控屬性目前值的函數。

   write — 可選,如果聲明将使你的依賴監控屬性可寫,别的代碼如果這個可寫功能寫入新值,通過自定義邏輯将值再寫入各個基礎的監控屬性上。

   owner — 可選,如果聲明,它就是KO調用read或write的callback時用到的this。檢視“管理this”擷取更新資訊。

 例2:Value轉換器

 有時候你可能需要顯示一些不同格式的資料,從基礎的資料轉化成顯示格式。比如,你存儲價格為float類型,但是允許使用者編輯的字段需要支援貨币機關和小數點。你可以用可寫的依賴監控屬性來實作,然後解析傳入的資料到基本 float類型裡:

然後我們綁定formattedPrice到text box上:

是以,不管使用者什麼時候輸入新價格,輸入什麼格式,text box裡會自動更新為帶有2位小數點和貨币符号的數值。這樣使用者可以看到你的程式有多聰明,來告訴使用者隻能輸入2位小數,否則的話自動删除多餘的位數,當然也不能輸入負數,因為write的callback函數會自動删除負号。

例3:過濾并驗證使用者輸入

例1展示的是寫操作過濾的功能,如果你寫的值不符合條件的話将不會被寫入,忽略所有不包括空格的值。

再多走一步,你可以聲明一個監控屬性isValid 來表示最後一次寫入是否合法,然後根據真假值顯示相應的提示資訊。稍後仔細介紹,先參考如下代碼:

… 按照如下格式聲明綁定元素:

現在,acceptedNumericValue 将隻接受數字,其它任何輸入的值都會觸發顯示驗證資訊,而會更新acceptedNumericValue。

依賴跟蹤如何工作的

新手沒必要知道太清楚,但是進階開發人員可以需要知道為什麼依賴監控屬性能夠自動跟蹤并且自動更新UI…

事實上,非常簡單,甚至說可愛。跟蹤的邏輯是這樣的:

當你聲明一個依賴監控屬性的時候,KO會立即調用執行函數并且擷取初始化值。

當你的執行函數運作的時候,KO會把所有需要依賴的依賴屬性(或者監控依賴屬性)都記錄到一個Log清單裡。

執行函數結束以後,KO會向所有Log裡需要依賴到的對象進行訂閱。訂閱的callback函數是重新運作你的執行函數。然後回頭重新執行上面的第一步操作(并且登出不再使用的訂閱)。

最後KO會通知上遊所有訂閱它的訂閱者,告訴它們我已經設定了新值。

所有說,KO不僅僅是在第一次執行函數執行時候探測你的依賴項,每次它都會探測。舉例來說,你的依賴屬性可以是動态的:依賴屬性A代表你是否依賴于依賴屬性B或者C,這時候隻有當A或者你目前的選擇B或者C改變的時候執行函數才重新執行。你不需要再聲明其它的依賴:運作時會自動探測到的。

另外一個技巧是:一個模闆輸出的綁定是依賴監控屬性的簡單實作,如果模闆讀取一個監控屬性的值,那模闆綁定就會自動變成依賴監控屬性依賴于那個監控屬性,監控屬性一旦改變,模闆綁定的依賴監控屬性就會自動執行。嵌套的模闆也是自動的:如果模闆X render模闆 Y,并且Y需要顯示監控屬性Z的值,當Z改變的時候,由于隻有Y依賴它,是以隻有Y這部分進行了重新繪制(render)。

如果你要探測和響應一個對象的變化,你應該用observables。如果你需要探測和響應一個集合對象的變化,你應該用observableArray 。在很多場景下,它都非常有用,比如你要在UI上需要顯示/編輯的一個清單資料集合,然後對集合進行添加和删除。

關鍵點:監控數組跟蹤的是數組裡的對象,而不是這些對象自身的狀态。

簡單說,将一對象放在observableArray 裡不會使這個對象本身的屬性變化可監控的。當然你自己也可以聲明這個對象的屬性為observable的,但它就成了一個依賴監控對象了。一個observableArray 僅僅監控他擁有的對象,并在這些對象添加或者删除的時候發出通知。

預加載一個監控數組observableArray

如果你想讓你的監控數組在開始的時候就有一些初始值,那麼在聲明的時候,你可以在構造器裡加入這些初始對象。例如:

從observableArray裡讀取資訊

一個observableArray其實就是一個observable的監控對象,隻不過他的值是一個數組(observableArray還加了很多其他特性,稍後介紹)。是以你可以像擷取普通的observable的值一樣,隻需要調用無參函數就可以擷取自身的值了。 例如,你可以像下面這樣擷取它的值:

理論上你可以使用任何原生的JavaScript數組函數來操作這些數組,但是KO提供了更好的功能等價函數,他們非常有用是因為:

相容所有浏覽器。(例如indexOf不能在IE8和早期版本上使用,但KO自己的indexOf 可以在所有浏覽器上使用)

在數組操作函數方面(例如push和splice),KO自己的方式可以自動觸發依賴跟蹤,并且通知所有的訂閱者它的變化,然後讓UI界面也相應的自動更新。

文法更友善,調用KO的push方法,隻需要這樣寫:myObservableArray.push(...)。 比如原生數組的myObservableArray().push(...)好用多了。

下面講解的均是observableArray的讀取和寫入的相關函數。

indexOf

indexOf 函數傳回的是第一個等于你參數數組項的索引。例如:myObservableArray.indexOf('Blah')将傳回以0為第一個索引的第一個等于Blah的數組項的索引。如果沒有找到相等的,将傳回-1。

slice

slice函數是observableArray相對于JavaScript 原生函數slice的等價函數(傳回給定的從開始索引到結束索引之間所有的對象集合)。 調用myObservableArray.slice(...)等價于調用JavaScript原生函數(例如:myObservableArray().slice(...))。

操作observableArray

observableArray 展現的是數組對象相似的函數并通知訂閱者的功能。

pop, push, shift, unshift, reverse, sort, splice

所有這些函數都是和JavaScript數組原生函數等價的,唯一不同的數組改變可以通知訂閱者:

    myObservableArray.push('Some new value') 在數組末尾添加一個新項

    myObservableArray.pop() 删除數組最後一個項并傳回該項

    myObservableArray.unshift('Some new value') 在數組頭部添加一個項

    myObservableArray.shift() 删除數組頭部第一項并傳回該項

    myObservableArray.reverse() 翻轉整個數組的順序

    myObservableArray.sort() 給數組排序

        預設情況下,是按照字元排序(如果是字元)或者數字排序(如果是數字)。

        你可以排序傳入一個排序函數進行排序,該排序函數需要接受2個參數(代表該數組裡需要比較的項),如果第一個項小于第二個項,傳回-1,大于則傳回1,等于傳回0。例如:用lastname給person排序,你可以這樣寫:myObservableArray.sort (function (left, right) {return left.lastName == right.lastName? 0: (left.lastName < right.lastName? -1: 1) })

     myObservableArray.splice() 删除指定開始索引和指定數目的數組對象元素。例如myObservableArray.splice(1, 3) 從索引1開始删除3個元素(第2,3,4個元素)然後将這些元素作為一個數組對象傳回。

remove和removeAll

observableArray 添加了一些JavaScript數組預設沒有但非常有用的函數:

    myObservableArray.remove(someItem) 删除所有等于someItem的元素并将被删除元素作為一個數組傳回

    myObservableArray.remove(function(item) { return item.age < 18 }) 删除所有age屬性小于18的元素并将被删除元素作為一個數組傳回

    myObservableArray.removeAll(['Chad', 132, undefined]) 删除所有等于'Chad', 123, or undefined的元素并将被删除元素作為一個數組傳回

destroy和destroyAll(注:通常隻和和Ruby on Rails開發者有關)

destroy和destroyAll函數是為Ruby on Rails開發者友善使用為開發的:

    myObservableArray.destroy(someItem) 找出所有等于someItem的元素并給他們添加一個屬性_destroy,并指派為true

    myObservableArray.destroy(function(someItem) { return someItem.age < 18 }) 找出所有age屬性小于18的元素并給他們添加一個屬性_destroy,并指派為true

    myObservableArray.destroyAll(['Chad', 132, undefined]) 找出所有等于'Chad', 123, 或undefined 的元素并給他們添加一個屬性_destroy,并指派為true

那麼,_destroy是做什麼用的?正如我提到的,這隻是為Rails 開發者準備的。在Rails 開發過程中,如果你傳入一個JSON對象,Rails 架構會自動轉換成ActiveRecord對象并且儲存到資料庫。Rails 架構知道哪些對象以及在資料庫中存在,哪些需要添加或更新, 标記_destroy為true就是告訴架構删除這條記錄。

注意的是:在KO render一個foreach模闆的時候,會自動隐藏帶有_destroy屬性并且值為true的元素。是以如果你的“delete”按鈕調用destroy(someItem) 方法的話,UI界面上的相對應的元素将自動隐藏,然後等你送出這個JSON對象到Rails上的時候,這個元素項将從資料庫删除(同時其它的元素項将正常的插入或者更新)。