Knockout 新版应用开发教程之Computed Observables

时间:2021-08-07 16:23:21

Computed Observables

如果你有监控属性firstName和lastName的话,此时如果你想要显示全名?

这个时候computed(以前叫做依赖)监控属性就出马了,这是一个函数用来依赖一个或者多个监控属性,并且当其中的任何一个依赖对象被改变的时候都将会自动更新。

例如,view model类

function AppViewModel() {
this.firstName = ko.observable('Bob');
this.lastName = ko.observable('Smith');
}

你可以增加一个computed计算依赖的来得到一个全名

function AppViewModel() {
// ... leave firstName and lastName unchanged ... this.fullName = ko.computed(function() {
return this.firstName() + " " + this.lastName();
}, this);
}

现在你可以绑定到它的UI元素,e.g

The name is <span data-bind="text: fullName"></span>

firstName或lastName发生改变它都会更新(不管谁改变,执行函数都会调用一次,不管改变成什么,他的值都会更新到UI或者其他依赖监控属性上)

管理this

初学者跳过,你只需要安装上面例子中的代码模式写就行了,无需知道/关注这个this。

ko.computed的第二个参数是什么,你是否很疑惑?

在前面的代码,我们在定义computed依赖的时候用到了this,没有它,将不能够引用到this.firstName()this.lastName()。

老练的Javascript程序员就觉得很平常,但是假如不怎么了解Javascript就会觉得难以理解(如C#和Java程序员不需要设置此值,但JavaScript呢,作用域是可以被改变的)

A popular convention that simplifies things

可以用一个简单的办法去简化这种行为

这是一种比较流行的办法用于避免追踪this:

如果你的模型的构造函数复制一个引用this到一个不同的变量(通常称为self),然后你可以用self的在你的模型和不必担心它被重新定义指的是别的东西。

比如说

function AppViewModel() {
var self = this; self.firstName = ko.observable('Bob');
self.lastName = ko.observable('Smith');
self.fullName = ko.computed(function() {
return self.firstName() + " " + self.lastName();
});
}

因为self是在函数的闭包中被捕获,在任何嵌套函数仍然是同一个,例如ko.computed的evaluator,当你设计到事件句柄的时候这个技巧更有用,可以看看更多的例子live examples.

Dependency chains just work

依赖链的工作

当然,你希望你能创建一个计算监控属性链,例如,你可以这样

  • 监控属性items表述一组列表项
  • 监控属性selectedIndexes保存着被用户选上的列表项的索引
  • 依赖监控属性selectedItems 返回的是selectedIndexes 对应的列表项数组
  • 另一个依赖监控属性返回的true或false依赖于 selectedItems 的各个列表项是否包含一些属性(例如,是否新的或者还未保存的)。一些UI element(像按钮的启用/禁用)的状态取决于这个值)。
  • 然后,items或者selectedIndexes 的改变将会影响到所有依赖监控属性的链,所有绑定这些属性的UI元素都会自动更新。多么整齐与优雅!

可写的计算监控属性

初学者跳过,可写的computed observables是比较高级的了,在大多情况下都是用不到的

当你学到了,计算监控属性的值是通过计算其他监控属性得到,在感觉上计算监控属性正常情况下仅仅是只读的。

这样看起来很奇怪,那么,computed observables是否可以支持可写呢?

你只需要提供自己的回调函数做一些事情,在写值的时候。

Example 1: Decomposing user input

分解用户的输入

返回到经典的 “first name + last name = full name” 实例,你可以把事情返过来看:

fullName给计算监控属性写入写东西,所以你能直接编辑出全名,让用户直接输入姓名全称,然后输入的值将被解析并映射写入到基本的监控属性firstName和lastName上:

function MyViewModel() {
this.firstName = ko.observable('Planet');
this.lastName = ko.observable('Earth'); this.fullName = ko.computed({
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: this
});
} ko.applyBindings(new MyViewModel());

在这里例子,write的回调句柄传入了一个值被分解后传入到“firstName” 和“lastName”  上,并且写的那些值会返回给相关的监控属性

跟平常一样将这个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>

这是一个Hello World 例子的反例子,姓和名都不可编辑,相反姓和名组成的姓名全称却是可编辑的。

之前的视图模型的代码演示单个参数的语法是为了初始化计算监控属性,看computed observable reference 以下全部可用选项的列表

Example 2: A value converter

转化一个value

有时候你可能需要显示一些不同格式的数据,从基础的数据转化成显示格式。

比如,你存储价格为float类型,但是允许用户编辑的字段需要支持货币单位和小数点。

你可以用可写的依赖监控属性来实现,然后解析传入的数据到基本 float类型里:

function MyViewModel() {
this.price = ko.observable(25.99); this.formattedPrice = ko.computed({
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: this
});
} ko.applyBindings(new MyViewModel());

然后我们绑定formattedPrice到text box上:

<p>Enter bid price: <input data-bind="value: formattedPrice"/></p>

所以,不管用户什么时候输入新价格,输入什么格式,text box里会自动更新为带有2位小数点和货币符号的数值。

这样用户可以看到你的程序有多聪明,来告诉用户只能输入2位小数,否则的话自动删除多余的位数,当然也不能输入负数,因为write的callback函数会自动删除负号。

Example 3: Filtering and validating user input

过滤并验证用户输入

例1展示的是写操作过滤的功能,如果你写的值不符合条件的话将不会被写入,忽略所有不包括空格的值。

再多走一步,你可以声明一个监控属性isValid 来表示最后一次写入是否合法,然后根据真假值显示相应的提示信息。

稍后仔细介绍,先参考如下代码:

function MyViewModel() {
this.acceptedNumericValue = ko.observable(123);
this.lastInputWasValid = ko.observable(true); this.attemptedValue = ko.computed({
read: this.acceptedNumericValue,
write: function (value) {
if (isNaN(value))
this.lastInputWasValid(false);
else {
this.lastInputWasValid(true);
this.acceptedNumericValue(value); // Write to underlying storage
}
},
owner: this
});
} ko.applyBindings(new MyViewModel());

… 按照如下格式声明绑定元素:

<p>Enter a numeric value: <input data-bind="value: attemptedValue"/></p>
<div data-bind="visible: !lastInputWasValid()">That's not a number!</div>

现在,acceptedNumericValue 将只接受数字,其它任何输入的值都会触发显示验证信息,而会更新acceptedNumericValue。

备注:上面的例子显得杀伤力太强了,更简单的方式是在<input>上使用jQuery Validation和number class。

Knockout可以和jQuery Validation一起很好的使用,参考例子:grid editor

当然,上面的例子依然展示了一个如何使用自定义逻辑进行过滤和验证数据,如果验证很复杂而jQuery Validation很难使用的话,你就可以用它。

How dependency tracking works

依赖跟踪如何工作的

初学者可以跳过,但是高级开发人员可以需要知道为什么依赖监控属性能够自动跟踪并且自动更新UI…

其实很简单而且可爱的,这个跟踪的算法是这样的:

  1. 当你声明一个依赖监控属性的时候,KO会立即调用执行函数并且获取初始化值。
  2. 当你的执行函数运行的时候,KO会把任何在监控属性(或者计算监控属性))读到的值都会都记录到一个Log列表里。
  3. 执行函数结束以后,KO会向所有Log里需要依赖到的对象进行订阅。订阅的callback函数是重新运行你的执行函数。然后回头重新执行上面的第一步操作(并且注销不再使用的订阅)。
  4. 最后KO会通知所有订阅它的订阅者,告诉它们我已经设置了新值。

所有说,KO不仅仅是在第一次执行函数执行时候探测你的依赖项,每次它都会探测。举例来说,你的依赖属性可以是动态的:依赖属性A代表你是否依赖于依赖属性B或者C,这时候只有当A或者你当前的选择B或者C改变的时候执行函数才重新执行。你不需要再声明其它的依赖:运行时会自动探测到的。

另外一个技巧是:一个模板输出的绑定是依赖监控属性的简单实现,如果模板读取一个监控属性的值,那模板绑定就会自动变成依赖监控属性依赖于那个监控属性,监控属性一旦改变,模板绑定的依赖监控属性就会自动执行。

Controlling dependencies using peek

使用Peek控制依赖

Knockout’s自动跟踪依赖通常下是你想要的。但是你可能有时候需要控制某一个监控属性去更新你的计算依赖属性,特别是如果你的计算依赖可执行一些操作,

比如Ajax请求,那么peek函数就能够让你访问一个observable或者computed observable而不是创建一个依赖

在下面的例子,一个

ko.computed(function() {
var params = {
page: this.pageIndex(),
selected: this.selectedItem.peek()
};
$.getJSON('/Some/Json/Service', params, this.currentPageData);
}, this);

注释:加入你不想要依赖属性太频繁的更新,你可以看 throttle extender.

Determining if a property is a computed observable

假如有一个属性是依赖属性

在一些场景中,如果你是处理一个依赖属性它是有用的编程方式,Knockout提供一个应用函数

ko.isComputed 将会帮助你解决这些情况

例如,数据从服务器返回回来,你可以要排除依赖属性

for (var prop in myObject) {
if (myObject.hasOwnProperty(prop) && !ko.isComputed(myObject[prop])) {
result[prop] = myObject[prop];
}
}

此外,Knockout 提供了类似的功能,能够对监控属性和依赖属性起到作用

  • ko.isObservable:返回true的,监控属性,监控数组和所有的依赖属性
  • ko.isWriteableObservable:返回true, 监控属性,监控数组,和 可写的依赖属性

Computed Observable Reference

引用依赖属性

一个依赖属性可以有下面的形式构造出来:

  1. ko.computed( evaluator [, targetObject, options] ) 最常见的情况,这种形式的支持创建一个依赖属性
  • evaluator — 一个函数,用来求出依赖属性当前的值
  • targetObject — 就是回调函数中,引用当前的this,要指定作用域,详情看managing this
  • options — 为依赖属性的配置更多的属性
  • ko.computed( options ) 创建一个依赖属性,传入的是一个单个对象:
    • read — 必选,一个用来执行取得依赖监控属性当前值的函数。
    • write — 可选,如果声明的依赖属性是可写的,那么这个函数接受一个值,那么其他代码将会试着写入到依赖属性,过自定义逻辑将值再写入各个基础的监控属性上。
    • owner — 可选,如果声明,它就是KO调用read或write的callback时用到的this。
    • deferEvaluation — 可选,假如是true,那么依赖属性的值你不能获取,默认情况下,依赖属性获取这个值的话会立刻创建
    • disposeWhen — 可选,待翻译,等分析源码的时候补上
    • disposeWhenNodeIsRemoved — 可选,待翻译,等分析源码的时候补上

    依赖属性可提供以下功能

    • dispose() — 手动配置依赖属性,清除所有订阅依赖,如果你想要停止依赖属性,当正在更新或者想要清除依赖属性的内存
    • extend(extenders) — 给依赖属性扩展一些内容
    • getDependenciesCount() — 返回当前被依赖属性依赖的数量
    • getSubscriptionsCount() — 返回当前依赖属性的订阅数量(或者从其他计算机的依赖属性或手动订阅)
    • isActive() — 返回依赖属性支持更新,如果没有依赖关系,将是无效的
    • peek() — 返回当前没有创建依赖关系的值(看 peek
    • subscribe( callback [,callbackTarget, event] ) — 手工注册依赖通知

    有些地方比较拗口,等看完源码后就能补准翻译了~~