(九)Knockout 进一步技术

加载和保存 JSON 数据

Knockout允许您实现复杂的客户端交互,但是几乎所有web应用程序还需要与服务器交换数据,或者至少要序列化数据以供本地存储。交换或存储数据最方便的方式是JSON格式——目前大多数Ajax应用程序都使用这种格式。

加载或保存数据

Knockout 并不强制您使用任何特定的技术来加载或保存数据。您可以使用适合您所选择的服务器端技术的任何方便的机制。最常用的机制是jQuery的Ajax助手方法,如getJSON、post和Ajax。您可以从服务器获取数据:

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

… 或者可以将数据发送到服务器r:

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函数,因此如果不进行额外的工作,就不能始终干净地序列化。

为了便于序列化视图模型数据,包括可观察对象等,Knockout包括两个帮助函数:

  • ko.toJS — 这个克隆您的view mode的对象图,替换每个可观察对象的当前值,这样您就得到了一个纯拷贝,它只包含您的数据,没有与Knockout相关的工件。
  • ko.toJSON — 这将生成一个JSON字符串,表示view model 的数据。在内部,它只是在view model上调用 ko.toJS 然后在结果上使用浏览器的原生JSON序列化器。注意:对于没有原生JSON序列化器的旧浏览器(如ie7或更早版本),要使其工作,还必须引用 json2.js库。

例如,定义一个视图模型,如下所示:

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字符串,如下所示:

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,如下所示:

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中,并绑定到您的视图模型,如:

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

使用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);
posted @ 2019-05-13 16:22  【唐】三三  阅读(568)  评论(0编辑  收藏  举报