Computed Observables
如果您有一个用于firstName
的 observable 对象,和一个用于lastName
的可观察对象,并且您想显示全名,该怎么办?这就是计算可观察性的用武之地——这些函数依赖于一个或多个其他可观察性,并且当这些依赖关系中的任何一个发生变化时,都会自动更新。
例如,给定以下视图模型类,
function AppViewModel() {
this.firstName = ko.observable('Bob');
this.lastName = ko.observable('Smith');
}
您可以添加一个 computed observable 来返回全名:
function AppViewModel() {
// ... 让 firstName 和 lastName 不变 ...
this.fullName = ko.computed(function() {
return this.firstName() + " " + this.lastName();
}, this);
}
现在您可以将UI元素绑定到它,例如:
The name is <span data-bind="text: fullName"></span>
1.1 依赖链是有效的
当然,如果您愿意,您可以创建整个 computed observables 链。例如,你可能有:
- 表示一组
items
的 observable 的称为项的 items - 另一个 observable 称为
selectedIndexes
,它存储用户选择的项索引 - 一个名为
selectedItems
的 computed observable,返回与所选索引对应的项对象数组 - 另一个computed observable,返回
true
或false
,这取决于selectedItems
是否具有某些属性(如new或unsaved)。一些UI元素,比如按钮,可能会基于这个值启用或禁用。
对 items
或selectedIndexes
的更改将波及computed observables链,而computed observables链又将更新绑定到它们的任何UI元素。
1.2 管理 ‘this’
给ko.computed
的第二个参数(我们在上面的例子中通过this在哪里)在计算 computed observable 时定义了this
值。如果不传递它,就不可能引用this.firstName()
或this.lastName()
。经验丰富的JavaScript程序员会认为这是显而易见的,但如果您仍然在学习JavaScript,这可能看起来很奇怪。(像c#和Java这样的语言从不期望程序员为此设置值,但是JavaScript会,因为它的函数本身在默认情况下不属于任何对象。)
一种简化事物的流行惯例
也有一种通用的简化方式,即将this在构造函数一开始就赋给另一个变量,这样在之后需要用到this的部分即可以通过调用另一个变量来实现:
function MyViewModel() {
var self = this;
self.firstName = ko.observable("Chiaki");
self.lastName = ko.observable("Izumi");
self.fullName = ko.computed(function() {
return self.firstName()+ " " + self.lastName();
});
}
个人分析,之所以能够通过这种方式来简化,跟javascript中的this机制有关,可能在javascript中每当遇到一个this的时候就分析当前的object到底是哪个,进而对this进行替代,但是进入到ko.computed函数里面之后,由于函数并不算是object的一部分,this的值也就不再是当前的object(MyViewModel),而变成了window,使用self以后就涉及到闭包的问题了,使得self的值并不会更改,这个可以留作以后研究。
1.3 Pure computed observables
如果computed observable知识基于一些observable的简单计算的话(其求值器不直接修改其他对象或状态),使用pureComputed
会比computed更好,如下:
self.fullName = ko.pureComputed(function() {
return self.firstName()+ " " + self.lastName();
})
1.4 强制计算的监控对象始终通知订阅者
我们也可以对computed或是pureComputed进行强制订阅,如下:
当 computed observable 返回一个基本值(数字、字符串、布尔值或null)时,通常只有当值实际更改时才会通知可观察对象的依赖关系。
this.fullName = ko.pureComputed(function() {
return this.firstName() + " " + this.lastName();
}, this).extend({notify: "always"});
1.5 延迟和/或禁止更改通知 rateLimit
同样的,可以通过调用extend方法中的rateLimit属性来指定响应的延时。
// Ensure updates no more than once per 50-millisecond period
myViewModel.fullName.extend({ rateLimit: 50 });
1.6 确定一个属性是否是计算监控的 isComputed
for (var prop in myObject) {
if (myObject.hasOwnProperty(prop) && !ko.isComputed(myObject[prop])) {
result[prop] = myObject[prop];
}
}
在某些时候,我们可能需要判定某个变量到底是不是computed observable,这时可以用到ko.isComputed来进行判断,类似的方法还包括isObservablem,isWritableObservable等,其中
- isObservable对于observables、observable arrays、computed observables均会返回true;
- isWritableObservable对于observables、observable arrays、writable computed observables均会返回true,这部分可以留作以后研究。
1.7 当计算监控属性(computed observables)仅在UI中使用时
参考:
官网,computedObservables
CharlieYuki,KnockoutJs学习笔记(三),Using computed observables
2 可写计算监控属性 Writable computed observables
初学者可能希望跳过这一节——可写的计算监控属性是相当高级的,在大多数情况下是不必要的。
通常,计算监控属性的值是从其他监控属性计算出来的,因此是只读的。那么,可能看起来令人惊讶的是,使计算出的监控属性数据可写是可能的。您只需要提供您自己的回调函数,它对写入的值执行一些合理的操作。
您可以使用与常规可写可计算可观察对象完全一样的可写可计算可观察对象,并使用您自己的自定义逻辑拦截所有的读和写。与observables一样,您可以使用链接语法将值写入模型对象上的多个可观察或计算的可观察属性。例如:
myViewModel.fullName('Joe Smith').age(50).
例1:分解用户的输入
返回到经典的“first name + last name = full name” 例子上,你可以让事情调回来看: 让依赖监控属性fullName可写,让用户直接输入姓名全称,然后输入的值将被解析并映射写入到基本的监控属性firstName和lastName上:
var viewModel = {
firstName: ko.observable("Planet"),
lastName: ko.observable("Earth")
};
viewModel.fullName = ko.dependentObservable({
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: viewModel
});
这个例子里,写操作的callback接受写入的值,把值分离出来,分别写入到“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 例子的反例子,姓和名都不可编辑,相反姓和名组成的姓名全称却是可编辑的。
上面的view model演示的是通过一个简单的参数来初始化依赖监控属性。你可以给下面的属性传入任何JavaScript对象:
- read — 必选,一个用来执行取得依赖监控属性当前值的函数。
- write — 可选,如果声明将使你的依赖监控属性可写,别的代码如果这个可写功能写入新值,通过自定义逻辑将值再写入各个基础的监控属性上。
- owner — 可选,如果声明,它就是KO调用read或write的callback时用到的this。查看“管理this”获取更新信息。
例2:全选/反选
当向用户显示一个可选择项列表时,包含一个选择或取消选择所有项的方法通常很有用。这可以非常直观地用一个布尔值表示,该值表示是否选择了所有项。当设置为true
,它将选择所有项目,当设置为false
,它将取消选择他们。
Source code: View
<div class="heading">
<input type="checkbox" data-bind="checked: selectedAllProduce" title="Select all/none"/> Produce
</div>
<div data-bind="foreach: produce">
<label>
<input type="checkbox" data-bind="checkedValue: $data, checked: $parent.selectedProduce"/>
<span data-bind="text: $data"></span>
</label>
</div>
Source code: View model
function MyViewModel() {
this.produce = [ 'Apple', 'Banana', 'Celery', 'Corn', 'Orange', 'Spinach' ];
this.selectedProduce = ko.observableArray([ 'Corn', 'Orange' ]);
this.selectedAllProduce = ko.pureComputed({
read: function () {
// Comparing length is quick and is accurate if only items from the
// main array are added to the selected array.
return this.selectedProduce().length === this.produce.length;
},
write: function (value) {
this.selectedProduce(value ? this.produce.slice(0) : []);
},
owner: this
});
}
ko.applyBindings(new MyViewModel());
例3:Value转换器
有时候你可能需要显示一些不同格式的数据,从基础的数据转化成显示格式。比如,你存储价格为float类型,但是允许用户编辑的字段需要支持货币单位和小数点。你可以用可写的依赖监控属性来实现,然后解析传入的数据到基本 float类型里:
Source code: View
<div>Enter bid price: <input data-bind="value: formattedPrice"/></div>
<div>(Raw value: <span data-bind="text: price"></span>)</div>
Source code: View model
function MyViewModel() {
this.price = ko.observable(25.99);
this.formattedPrice = ko.pureComputed({
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());
现在,每当用户输入一个新价格时,文本框都会更新,以显示它的格式是货币符号和两位小数,无论输入的格式是什么。这给了一个很好的用户体验,因为用户看到了软件如何将他们的数据输入理解为一个价格。他们知道他们不能输入超过两位小数,因为如果他们尝试输入,额外的小数位就会被删除。类似地,它们不能输入负值,因为write回调去掉了任何负号。
例4:过滤并验证用户输入
例1展示的是写操作过滤的功能,如果你写的值不符合条件的话将不会被写入,忽略所有不包括空格的值。
再多走一步,你可以声明一个监控属性 isValid
来表示最后一次写入是否合法,然后根据真假值显示相应的提示信息。稍后仔细介绍,先参考如下代码:
Source code: View
<div>Enter a numeric value: <input data-bind="textInput: attemptedValue"/></div>
<div class="error" data-bind="visible: !lastInputWasValid()">That's not a number!</div>
<div>(Accepted value: <span data-bind="text: acceptedNumericValue"></span>)</div>
Source code: View model
function MyViewModel() {
this.acceptedNumericValue = ko.observable(123);
this.lastInputWasValid = ko.observable(true);
this.attemptedValue = ko.pureComputed({
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());
现在,acceptedNumericValue
将只接受数字,其它任何输入的值都会触发显示验证信息,而会更新acceptedNumericValue
。
备注:上面的例子显得杀伤力太强了,更简单的方式是在<input>
上使用jQuery Validation
和number class
。Knockout可以和jQuery Validation一起很好的使用,参考例子:grid editor 。当然,上面的例子依然展示了一个如何使用自定义逻辑进行过滤和验证数据,如果验证很复杂而jQuery Validation很难使用的话,你就可以用它。
3 依赖跟踪如何工作的
新手没必要知道太清楚,但是高级开发人员可以需要知道为什么依赖监控属性能够自动跟踪并且自动更新UI…
事实上,非常简单,甚至说可爱。跟踪的逻辑是这样的:
- 当你声明一个依赖监控属性的时候,KO会立即调用执行函数并且获取初始化值
- 当你的执行函数运行的时候,KO会把所有需要依赖的依赖属性(或者监控依赖属性)都记录到一个Log列表里。
- 执行函数结束以后,KO会向所有Log里需要依赖到的对象进行订阅。订阅的callback函数是重新运行你的执行函数。然后回头重新执行上面的第一步操作(并且注销不再使用的订阅)。
- 最后KO会通知上游所有订阅它的订阅者,告诉它们我已经设置了新值。
所以,KO不仅仅是在第一次执行函数执行时候探测你的依赖项,每次它都会探测。举例来说,你的依赖属性可以是动态的:依赖属性A代表你是否依赖于依赖属性B或者C,这时候只有当A或者你当前的选择B或者C改变的时候执行函数才重新执行。你不需要再声明其它的依赖:运行时会自动探测到的。
另外一个技巧是:一个模板输出的绑定是依赖监控属性的简单实现,如果模板读取一个监控属性的值,那模板绑定就会自动变成依赖监控属性依赖于那个监控属性,监控属性一旦改变,模板绑定的依赖监控属性就会自动执行。嵌套的模板也是自动的:如果模板X render模板 Y,并且Y需要显示监控属性Z的值,当Z改变的时候,由于只有Y依赖它,所以只有Y这部分进行了重新绘制(render)。
4 PureComputed
Pure computed observables相对于一般的computed observables,在性能和存储上有优势,这是因为pure computed observables在不存在订阅者的时候是不会保持订阅关系的。这也使得pure computed observables有如下两点特点:
- 可以防止没有被订阅的computed observables的存储泄露。
- 可以降低因重复计算未被订阅的computed observables而造成的运算过载。
一个pure computed observable能够依据它是否拥有订阅者而自动地在两种状态下切换:
1.当不存在订阅者的时候,pure computed observable会进入休眠状态,此时的它,会关闭所有依赖于它的订阅关系,同时也不会再追踪它所关联的observables。一旦处于休眠状态的computed observable的值被读取的话,它就需要重新计算以便以确保值得正确性。
2.当它拥有订阅者的时候,pure computed observable会进入监听状态。一旦进入监听状态,它会立即调用它的function和订阅程序来追踪它所关联的observables。在这种状态下,pure computed observables和普通的computed observables无异。更为详细的内容需参考How dependency tracking works部分。
按照文档的说明,选择pure computed observables有两条原则。一是computed observable在运算的时候不能产生副作用(不能对其他的observables产生影响);二是computed observable的值应该仅仅依赖于它所关联的observables的值,而不是其他隐含的信息。
Pure computed observables有两种定义方式:
this.fullName = ko.pureComputed(function() {
return this.firstName() + " " + this.lastName();
}, this);
或者
this.fullName = ko.computed(function() {
return this.firstName() + " " + this.lastName();
}, this, { pure: true });
4.1 参考:
Computed Observable 参考
下面的文档描述了如何构造和使用 Computed Observable。
构建一个 Computed Observable
-
ko.computed( evaluator [, targetObject, options] )
— 此表单支持创建 computed observable 的最常见情况。- evaluator — 用于computed observable 值的当前值的函数
- targetObject — 如果给定,则在KO调用回调函数时定义this的值。有关更多信息,请参见管理this功能的部分。
- options — An object with further properties for the computed observable. See the full list below.
-
ko.computed( options )
— This single parameter form for creating a computed observable accepts a JavaScript object with any of the following properties.