(九)Knockout 進一步技術

加載和保存 JSON 數據

Knockout容許您實現複雜的客戶端交互,可是幾乎全部web應用程序還須要與服務器交換數據,或者至少要序列化數據以供本地存儲。交換或存儲數據最方便的方式是JSON格式——目前大多數Ajax應用程序都使用這種格式。javascript

加載或保存數據

Knockout 並不強制您使用任何特定的技術來加載或保存數據。您可使用適合您所選擇的服務器端技術的任何方便的機制。最經常使用的機制是jQuery的Ajax助手方法,如getJSON、post和Ajax。您能夠從服務器獲取數據:css

$.getJSON("/some/url", function(data) { 
    // Now use this data to update your view models, 
    // and Knockout will update your UI automatically 
})

… 或者能夠將數據發送到服務器r:html

var data = /* Your data in JSON format - see below */;
$.post("/some/url", data, function(returnedData) {
    // This callback is executed if the post was successful     
})

將視圖模型數據轉換爲純JSON

視圖模型是JavaScript對象,所以在某種意義上,您可使用任何標準的JSON序列化器(好比JSON)將它們序列化爲JSON.stringify(現代瀏覽器中的一個本機函數)或json2.js庫。然而,您的視圖模型可能包含可觀察性、計算可觀察性和可觀察數組,這些數組被實現爲JavaScript函數,所以若是不進行額外的工做,就不能始終乾淨地序列化。java

爲了便於序列化視圖模型數據,包括可觀察對象等,Knockout包括兩個幫助函數:react

  • ko.toJS — 這個克隆您的view mode的對象圖,替換每一個可觀察對象的當前值,這樣您就獲得了一個純拷貝,它只包含您的數據,沒有與Knockout相關的工件。
  • ko.toJSON — 這將生成一個JSON字符串,表示view model 的數據。在內部,它只是在view model上調用 ko.toJS 而後在結果上使用瀏覽器的原生JSON序列化器。注意:對於沒有原生JSON序列化器的舊瀏覽器(如ie7或更早版本),要使其工做,還必須引用 json2.js庫。

例如,定義一個視圖模型,以下所示:git

var viewModel = {
    firstName : ko.observable("Bert"),
    lastName : ko.observable("Smith"),
    pets : ko.observableArray(["Cat", "Dog", "Fish"]),
    type : "Customer"
};
viewModel.hasALotOfPets = ko.computed(function() {
    return this.pets().length > 2
}, viewModel)

這包含可觀測值、計算的可觀測值、可觀測數組和普通值的混合。您可使用ko.toJSON將其轉換爲適合發送到服務器的JSON字符串,以下所示:github

var jsonData = ko.toJSON(viewModel);
 
// Result: jsonData is now a string equal to the following value
// '{"firstName":"Bert","lastName":"Smith","pets":["Cat","Dog","Fish"],"type":"Customer","hasALotOfPets":true}'

或者,若是您只想在序列化以前獲得簡單的JavaScript對象圖,請使用ko.toJS,以下所示:web

var plainJs = ko.toJS(viewModel);
 
// Result: plain js如今是一個純JavaScript對象,其中沒有任何可觀察的內容。這只是數據。
// The object is equivalent to the following:
//   {
//      firstName: "Bert",
//      lastName: "Smith",
//      pets: ["Cat","Dog","Fish"],
//      type: "Customer",
//      hasALotOfPets: true
//   }

請注意,ko.toJSON接受與 JSON.stringify相同的參數。例如,在調試Knockout應用程序時,擁有視圖模型數據的「實時」表示可能頗有用。要爲今生成格式良好的顯示,您能夠將spaces參數傳遞到ko.toJSON中,並綁定到您的視圖模型,如:ajax

<pre data-bind="text: ko.toJSON($root, null, 2)"></pre>

使用JSON更新視圖模型數據

若是您已經從服務器加載了一些數據,而且想要使用它來更新視圖模型,那麼最直接的方法就是本身動手。例如,json

// Load and parse the JSON
var someJSON = /* 忽略: 從服務器上以您想要的方式獲取它 */;
var parsed = JSON.parse(someJSON);
 
// Update view model properties
viewModel.firstName(parsed.firstName);
viewModel.pets(parsed.pets);

在許多場景中,這種直接方法是最簡單和最靈活的解決方案。固然,當您更新視圖模型上的屬性時,Knockout將負責更新可視UI以匹配它。

然而,許多開發人員更喜歡使用基於約定的方法來使用傳入數據更新視圖模型,而不須要爲每一個要更新的屬性手動編寫一行代碼。若是視圖模型具備許多屬性或深度嵌套的數據結構,這將是有益的,由於它能夠大大減小您須要編寫的手工映射代碼的數量。有關這項技術的更多細節,請參見 the knockout.mapping plugin插件。

使用擴展器來加強 observables

Knockout observables提供支持讀/寫值所需的基本功能,並在該值發生變化時通知訂閱者。 可是,在某些狀況下,您可能但願向可觀察對象添加其餘功能。 這可能包括向可觀察對象添加附加屬性,或者經過在可觀察對象前面放置可寫的計算可觀察對象來攔截寫入。 Knockout擴展器提供了一種簡單靈活的方法來對可觀察的這種類型的擴充。

如何建立擴展程序

建立擴展器須要向 ko.extenders 添加一個函數來延伸部分對象。該函數將可觀察對象自己做爲第一個參數,將任何選項做爲第二個參數。而後它能夠返回可觀察的,或者返回一些新的東西,好比一個計算的可觀察的,以某種方式使用原始的可觀察的。

這個簡單的logChange extender訂閱可觀察對象,並使用控制檯編寫任何更改以及可配置的消息。

ko.extenders.logChange = function(target, option) {
    target.subscribe(function(newValue) {
       console.log(option + ": " + newValue);
    });
    return target;
};

您能夠經過調用一個可觀察對象的extend函數並傳遞一個包含logChange屬性的對象來使用這個擴展程序。

this.firstName = ko.observable("Bob").extend({logChange: "first name"});

若是 firstName observable的值被更改成Ted,那麼控制檯將顯示firstName: Ted

Live Example 1: 強制輸入是數字

本例建立了一個擴展器,該擴展器強制將寫入到可觀察對象的數據四捨五入到可配置的精度級別。在這種狀況下,擴展器將返回一個新的可寫計算可觀察對象,該可寫計算可觀察對象將位於實際可觀察到的攔截寫以前。

Source code: View

<p><input data-bind="value: myNumberOne" /> (round to whole number)</p>
<p><input data-bind="value: myNumberTwo" /> (round to two decimals)</p>

Source code: View model

ko.extenders.numeric = function(target, precision) {
    //create a writable computed observable to intercept writes to our observable
    var result = ko.pureComputed({
        read: target,  //always return the original observables value
        write: function(newValue) {
            var current = target(),
                roundingMultiplier = Math.pow(10, precision),
                newValueAsNum = isNaN(newValue) ? 0 : +newValue,
                valueToWrite = Math.round(newValueAsNum * roundingMultiplier) / roundingMultiplier;
 
            //only write if it changed
            if (valueToWrite !== current) {
                target(valueToWrite);
            } else {
                //if the rounded value is the same, but a different value was written, force a notification for the current field
                if (newValue !== current) {
                    target.notifySubscribers(valueToWrite);
                }
            }
        }
    }).extend({ notify: 'always' });
 
    //initialize with current value to make sure it is rounded appropriately
    result(target());
 
    //return the new computed observable
    return result;
};
 
function AppViewModel(one, two) {
    this.myNumberOne = ko.observable(one).extend({ numeric: 0 });
    this.myNumberTwo = ko.observable(two).extend({ numeric: 2 });
}
 
ko.applyBindings(new AppViewModel(221.2234, 123.4525));

注意,爲了自動從UI中刪除被拒絕的值,必須在計算的觀察對象上使用.extend({notify: 'always'})。若是沒有這個,用戶可能會輸入一個無效的newValue,當四捨五入時,它會給出一個未更改的valueToWrite。而後,因爲模型值不會更改,因此不會有更新UI中的文本框的通知。使用{notify: 'always'}會致使文本框刷新(刪除被拒絕的值),即便計算的屬性沒有更改值。

Live Example 2: 向可觀察對象添加驗證

這個例子建立了一個擴展器,它容許根據須要對可觀察對象進行標記。該擴展器不返回新對象,而只是向現有的可觀察對象添加額外的子可觀察對象。因爲可觀察對象是函數,它們實際上能夠有本身的屬性。然而,當視圖模型被轉換成JSON時,子可觀察對象將被刪除,只剩下實際可觀察對象的值。這是一種添加僅與UI相關且不須要發送回服務器的附加功能的好方法。

Source code: View

<p data-bind="css: { error: firstName.hasError }">
    <input data-bind='value: firstName, valueUpdate: "afterkeydown"' />
    <span data-bind='visible: firstName.hasError, text: firstName.validationMessage'> </span>
</p>
<p data-bind="css: { error: lastName.hasError }">
    <input data-bind='value: lastName, valueUpdate: "afterkeydown"' />
    <span data-bind='visible: lastName.hasError, text: lastName.validationMessage'> </span>
</p>

Source code: View model

ko.extenders.required = function(target, overrideMessage) {
    //add some sub-observables to our observable
    target.hasError = ko.observable();
    target.validationMessage = ko.observable();
 
    //define a function to do validation
    function validate(newValue) {
       target.hasError(newValue ? false : true);
       target.validationMessage(newValue ? "" : overrideMessage || "This field is required");
    }
 
    //initial validation
    validate(target());
 
    //validate whenever the value changes
    target.subscribe(validate);
 
    //return the original observable
    return target;
};
 
function AppViewModel(first, last) {
    this.firstName = ko.observable(first).extend({ required: "Please enter a first name" });
    this.lastName = ko.observable(last).extend({ required: "" });
}
 
ko.applyBindings(new AppViewModel("Bob","Smith"));

應用多個擴展程序

在對可觀察對象的.extend方法的一次調用中能夠應用多個擴展程序。

this.firstName = ko.observable(first).extend({ required: "Please enter a first name", logChange: "first name" });

在這種狀況下,requiredlogChange擴展器都將針對咱們的observable執行。

Deferred updates 推遲更新

Enabling deferred updates

Deferred updates are turned off by default to provide compatibility with existing applications. To use deferred updates for your application, you must enable it before initializing your viewmodels by setting the following option:

Example: Avoiding multiple UI updates

The following is a contrived example to demonstrate the ability of deferred updates to eliminate UI updates of intermediate values and how this can improve performance.

Using deferred updates for specific observables

Even if you don’t enable deferred updates for your whole application, you can still benefit from this feature by specifically making certain observables deferred. This is done using the deferred extender:

Example: Avoiding multiple Ajax requests

The following model represents data that you could render as a paged grid:

Forcing deferred notifications to happen early

Although deferred, asynchronous notifications are generally better because of fewer UI updates, it can be a problem if you need to update the UI immediately. Sometimes, for proper functionality, you need an intermediate value pushed to the UI. You can accomplish this using the ko.tasks.runEarly method. For example:

Forcing deferred notifications to happen early

Although deferred, asynchronous notifications are generally better because of fewer UI updates, it can be a problem if you need to update the UI immediately. Sometimes, for proper functionality, you need an intermediate value pushed to the UI. You can accomplish this using the ko.tasks.runEarly method. For example:

Rate-limiting observable notifications 限速可觀察量

Applying the rateLimit extender

rateLimit supports two parameter formats:

Example 1: The basics

Consider the observables in the following code:

Example 2: Doing something when the user stops typing

In this live example, there’s an instantaneousValue observable that reacts immediately when you press a key. This is then wrapped inside a delayedValue computed observable that’s configured to notify only when changes stop for at least 400 milliseconds, using the notifyWhenChangesStop rate-limit method.

Try it:

Custom rate-limit methods

Knockout 3.5 introduced the ability to specify a custom rate-limit method by passing a function to the rateLimit extender rather than just a string. The function is called with three parameters (function, timeout, options) and must return a new, rate-limited function. Whenever the observable has a possibly new value to notify, it will call the returned function, which should then call the original function after some delay based on the rules of the custom method. For example, here is a function that implements debounce but also immediately notifies the initial value:

Special consideration for computed observables

For a computed observable, the rate-limit timer is triggered when one of the computed observable’s dependencies change instead of when its value changes. The computed observable is not re-evaluated until its value is actually needed—after the timeout period when the change notification should happen, or when the computed observable value is accessed directly. If you need to access the value of the computed’s most recent evaluation, you can do so with the peek method.

Forcing rate-limited observables to always notify subscribers

When the value of any observable is primitive (a number, string, boolean, or null), the dependents of the observable are by default notified only when it is set to a value that is actually different from before. So, primitive-valued rate-limited observables notify only when their value is actually different at the end of the timeout period. In other words, if a primitive-valued rate-limited observable is changed to a new value and then changed back to the original value before the timeout period ends, no notification will happen.

If you want to ensure that the subscribers are always notified of an update, even if the value is the same, you would use the notify extender in addition to rateLimit:

Comparison with deferred updates

Knockout version 3.4.0 added support for deferred updates, which works similarly to rate-limiting by making notifications and updates asynchronous. But instead of using a timed delay, deferred updates are processed as soon as possible after the current task, before yielding for I/O, reflow, or redrawing. If you are upgrading to 3.4.0 and have code that uses a short rate-limit timeout (e.g., 0 milliseconds), you could modify it to use deferred updates instead:

Comparison with the throttle extender

If you’d like to migrate code from using the deprecated throttle extender, you should note the following ways that the rateLimit extender is different from the throttle extender.

When using rateLimit:

  1. Writes to observables are not delayed; the observable’s value is updated right away. For writable computed observables, this means that the write function is always run right away.
  2. All change notifications are delayed, including when calling valueHasMutated manually. This means you can’t use valueHasMutated to force a rate-limited observable to notify an un-changed value.
  3. The default rate-limit method is different from the throttle algorithm. To match the throttle behavior, use the notifyWhenChangesStop method.
  4. Evaluation of a rate-limited computed observable isn’t rate-limited; it will re-evaluate if you read its value.

Using unobtrusive event handlers

In most cases, data-bind attributes provide a clean and succinct way to bind to a view model. However, event handling is one area that can often result in verbose data-bind attributes, as anonymous functions were typically the recommended techinique to pass arguments. For example:

Live example: nested children

This example shows 「add」 and 「remove」 links on multiple levels of parents and children with a single handler attached unobtrusively for each type of link.

使用"fn"爲了添加自定義功能

有時,您可能會找到機會經過附加新功能到Knockout核心值類型來簡化代碼。您能夠在如下任何類型上定義自定義函數:

因爲繼承,若是您將一個函數附加到ko.subscribable,它也將在全部其餘函數上可用。若是將一個函數附加到ko.observable,它將被ko.observableArray繼承,但不會被ko.computed繼承。

要附加自定義函數,請將其添加到如下擴展點之一:

  • ko.subscribable.fn
  • ko.observable.fn
  • ko.observableArray.fn
  • ko.computed.fn

而後,您的自定義函數將對今後之後建立的全部該類型的值可用。

Note: 最好只將此可擴展性點用於真正適用於普遍場景的自定義函數。若是隻打算使用一次,則不須要向這些名稱空間添加自定義函數。

Example: 一個可觀察數組的過濾視圖

如下是定義filterByProperty 函數的方法,該函數將在全部後續建立的ko.observableArray實例中可用:

ko.observableArray.fn.filterByProperty = function(propName, matchValue) {
    return ko.pureComputed(function() {
        var allItems = this(), matchingItems = [];
        for (var i = 0; i < allItems.length; i++) {
            var current = allItems[i];
            if (ko.unwrap(current[propName]) === matchValue)
                matchingItems.push(current);
        }
        return matchingItems;
    }, this);
}

這將返回一個新的計算值,該值提供一個通過篩選的數組視圖,同時保持原始數組不變。由於過濾後的數組是計算可監控到的,因此每當底層數組發生更改時,都會從新計算它。

Source code: View

<h3>All tasks (<span data-bind="text: tasks().length"> </span>)</h3>
<ul data-bind="foreach: tasks">
    <li>
        <label>
            <input type="checkbox" data-bind="checked: done" />
            <span data-bind="text: title"> </span>
        </label>
    </li>
</ul>
 
<h3>Done tasks (<span data-bind="text: doneTasks().length"> </span>)</h3>
<ul data-bind="foreach: doneTasks">
    <li data-bind="text: title"></li>
</ul>

Source code: View model

function Task(title, done) {
    this.title = ko.observable(title);
    this.done = ko.observable(done);
}
 
function AppViewModel() {
    this.tasks = ko.observableArray([
        new Task('Find new desktop background', true),
        new Task('Put shiny stickers on laptop', false),
        new Task('Request more reggae music in the office', true)
    ]);
 
    // Here's where we use the custom function
    this.doneTasks = this.tasks.filterByProperty("done", true);
}
 
ko.applyBindings(new AppViewModel());

這不是強制性的

若是您傾向於大量過濾可觀察數組,那麼全局地向全部可觀察數組添加filterByProperty可能會使您的代碼更整潔。但若是隻是偶爾須要過濾,則能夠選擇不附加到ko.observableArray.fn,而只是手工構造doneTasks ,以下所示

this.doneTasks = ko.pureComputed(function() {
    var all = this.tasks(), done = [];
    for (var i = 0; i < all.length; i++)
        if (all[i].done())
            done.push(all[i]);
    return done;
}, this);
相關文章
相關標籤/搜索