Knockout 最棒的一個特點就是它的可擴展性。Knockout 存在大量的擴展點,包含大量的工具來創建我們的應用程序。許多開發者除了 Knockout 核心庫之外沒有使用任何其他的腳本庫 ( 甚至包括 jQuery ) 就創建了非常棒的站點。
Subscribables
在創建我們的庫存管理程序的時候,很容易發現在 Knockout 中 Observable 是一個核心對象。在 Observable,ObservableArray 和 Computed Observables 的底層是 Subscribable,Subscribable 是包含三個方法和一個 Subscriptions 數組的對象,這三個方法是:
- subscribe:這個方法添加一個訂閱到主題對象上,當訂閱的主題發出提醒的時候,訂閱就會被調用,默認的提醒類型是 change。
- notifySubscribers:這個方法調用所有的訂閱,並且會傳遞一個參數到訂閱的回調方法中。
- extend:這個方法為主題對象添加一個擴展
下面的代碼演示了前兩個方法的使用,實現了發布和訂閱。
var test = ko.observable(); // create a subscription for the "test-event" test.subscribe(function (val) { console.log(val); }, test, "test-event"); test.notifySubscribers("Hello World", "test-event");
Knockout 中訂閱的一個很酷的特性是可以混合到任何的 Javascript 對象中,下面的代碼演示了在普通代碼中使用的方式。
// Dummy Subscribable function PubSub() { // inherit Subscribable ko.subscribable.call(this); } // create an instance of our Subscribable var pubsub = new PubSub(); // make a subscription var subscription = pubsub.subscribe(function (val) { console.log(val); }, pubsub, 'test-topic'); pubsub.notifySubscribers("hello world", "test-topic"); // console: "hello world" // clean up things subscription.dispose();
在調用可訂閱對象的subscribe 方法之后,我們得到了一個 Subscription 對象,通常開發者可以忽略這個 subscribe 方法返回的訂閱對象,但是需要注意的是通過這個訂閱對象 Subscription ,我們可以釋放訂閱的回調方法和應用中對於回調的引用,直接調用 dispose 方法就可以確信你的程序不會造成內存的泄露。
現在,你已經理解了 Knockout 的一個核心處理機制,Subscribable,我們將學習 Knockout 如何擴展它的用法在其它的模式。
Observables
Knockout 的 Observable 對象是實現訂閱機制的頭等對象,也是 Knockout 庫中最為簡單,也最為強大的部分。下面的代碼演示了實現 Observable 的基本思想。
// Very Simple Knockout Observable Implementation // ko.observable is actually a function factory ko.observable = function (initialValue) { // private variable to hold the Observable's value var _latestValue = initialValue; // the actual "Observable" function function observable() { // one or more args, so it's a Write if (arguments.length > 0) { // set the private variable _latestValue = arguments[0]; // tell any subscribers that things have changed observable["notifySubscribers"](_latestValue); return this; // Permits chained assignments } else { // no args, so it's a Read // just hand back the private variable's value return _latestValue; } } // inherit from Subscribable ko.subscribable.call(observable); // return the freshly created Observable function return observable; };
這是 Knockout 中 observable 的一個簡單實現 ( 真正的實現代碼中包含了大量的強壯性處理代碼,驗證,依賴檢測等等 ),從這個實現中可以看到實際上 observable 是一個函數的工廠,當調用這個工廠方法的時候,它生成了一個新的函數。這里實現了一個簡單的 set/get API。如果你在不提供參數的情況下調用返回的函數,就會返回 _initialValue ,如果調用的時候提供了一個參數,就會設置 _initialValue ,並且通知所有的訂閱。
當你創建事件驅動的程序的時候,Observable 可以得到大量的應用。我們可以創建事件驅動的程序架構,僅僅依賴事件 ( 比如按鈕的點擊,或者輸入元素的變化等等 )而不是過程化的方法。
Observable Array
我在前面已經提到過可觀察的數組,而且提高還會深入討論這個特性。前面的示例中已經演示了 Observable 比較簡單,而可觀察的數組也一樣簡單。下面的代碼演示了實現的機制。
// Very Simple Knockout Observable Array Implementation // function factory for observable arrays ko.observableArray = function (initialValues) { // make sure we have an array initialValues = initialValues || []; // create a Knockout Observable around our Array var result = ko.observable(initialValues); // add our Observable Array member functions // like "push", "pop", and so forth ko.utils.extend(result, ko.observableArray['fn']); // hand back the Observable we've created return result; };
如你所見,實際上還是一個可觀察對象,除了這個可觀察對象的值是一個數組而已。另外還添加了匹配數組本地方法的擴展方法。Knockout 的作者已經幫我們完成的這些工作,使得開發者可以像使用普通的數組一樣使用可觀察的數組。
可觀察數組的一個擴展點是它的 fn 屬性,你可以添加自己的函數到這個屬性中,那么就可以使得應用中所有的可觀察數組都提供這個方法,下面的代碼演示了如何添加一個過濾方法來過濾數組中的元素。
ko.observableArray.fn['filter'] = function (filterFunc) { // get the array var underlyingArray = this(); var result = []; for (var i = 0; i < underlyingArray.length; i++) { var value = underlyingArray[i]; // execute filter logic if (filterFunc(value)) { result.push(value); } } return result; }; var list = ko.observableArray([1, 2, 3]); // filter down the list to only odd numbers var odds = list.filter(function (item) { return (item % 2 === 1); }); console.log(odds); // [1, 3]
使用 fn 函數在許多腳本庫中是常見的模式,包括 jQuery 腳本庫,Knockout 也不例外。fn 也存在於 Observable 中,所以你也可以通過同樣的方式進行擴展。
Computed Observable
計算出的可觀察對象可以說是 Knockout 中最強大的特性。在你學會使用 Observable 和 Observable Array 而不是用其他技術來創建視圖模型之后,已經是一個巨大的進步,但是,更棒的是你可以創建僅僅依賴於其他 Observable 和 Observalbe Array 的屬性,而且在底層屬性發生變化的時候,計算的結果會同時發生變化。
計算出的可觀察對象類似於普通的可觀察對象,除了沒有保存自己的值之外,它提供了一個方法來計算應該返回的值,這個函數僅僅在所依賴的底層對象發生變化的時候重新計算返回值,下面的代碼演示了實現的機制。
var a = ko.observable(1); var b = ko.observable(2); var sum = ko.computed(function () { var total = a() + b(); // let's log every time this runs console.log(total); return total; }); // console: 3 a(2); // console: 4 b(3); // console: 5 b(3); // (nothing logged)
計算出的可觀察對象有着非常有趣和巧妙的實現,你可以自己挖掘一下,初次之外,還有一些需要注意的要點。
首先,計算出的可觀察對象創建了一個依賴列表 ( 或者訂閱 ),當你的計算函數執行的時候,如果你希望依賴於某個對象,就需要保證在計算函數中調用這個 Observable ,下面的代碼演示了如何設置依賴和動態改變計算對象的依賴對象。
var dep1 = ko.observable(1); var dep2 = ko.observable(2); var skipDep1 = false; var comp = ko.computed(function () { dep2(); // register dep2 as dependency if (!skipDep1) { dep1(); // register dep1 as dependency } console.log('evaluated'); }); // console: evaluated dep1(99); // console: evaluated skipDep1 = true; dep2(98); // console: evaluated dep1(97); // (nothing logged)
一般來說,上面的代碼不是什么好注意,這使得代碼更難調試,使其他的開發人員更加難以直觀地意識到發生的狀況。相反,建議的處理方式是首先從依賴對象中獲取所有的值,然后,使用私有的變量來評估處理邏輯問題。
第二點,這一點使用所有的可觀察對象,訂閱的回調函數僅僅在屬性的值發生變化的時候,每個可觀察對象都擁有一個名為equalityComparer 的屬性,用來檢測屬性之前和之后的值是否不同,如果你注意前面的例子,最后一個調用什么都沒有做,這是因為第二次設置的值還是 3。默認的equalityComparer 實現僅僅比較 javascript 的基礎值,如果可計算對象依賴所有的對象,包括復雜的或者不復雜的,那么,這個函數就會執行多次,下面的代碼演示了這個重要的概念。
var depPrimitive = ko.observable(1); var depObj = ko.observable({ val: 1 }); var comp = ko.computed(function () { // register dependencies var prim = depPrimitive(); var obj = depObj(); console.log("evaluated"); }); // console: evaluated depPrimitive(1); // (nothing logged) var previous = depObj(); depObj(previous); // console: evaluated
重要的是理解這一點,很多 Knockout 的新手創建了復雜的可計算對象,導致應用的性能降低。
最后一點,計算的可觀察對象也可以擁有自己的 set/get ,這一點很有用,因為這允許我們在視圖模型上擁有私有的可觀察對象,可以通過公共的 set/get 類似於普通的可觀察對象一樣使用,下面的代碼演示了這個技術。
var _val = ko.observable(1); var vm = { val: ko.computed({ read: function () { return _val(); }, write: function (newVal) { _val(newVal); } }) }; vm.val(2); console.log(_val()); // console: 2 _val(3); console.log(vm.val()); // console: 3