入门

监控属性

使用内置绑定

控制文本和外观

绑定逻辑控制

处理表单属性

解析模板

高级应用

插件

更多信息

自动计算属性(Computed Observables)

如果你已经有了监控属性(observable) for firstNamelastName,你想显示全称怎么办?这就需要用到 自动计算属性(computed observables) 了 – 这些函数是一个或多个监控属性, 如果他们的依赖对象改变,他们会自动跟着改变。

What if you’ve got an observable for firstName, and another for lastName, and you want to display the full name? That’s where computed observables come in - these are functions that are dependent on one or more other observables, and will automatically update whenever any of these dependencies change.

例如,下面的view model,

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

…你可以添加一个自动计算属性来返回姓名全称:

  1. function AppViewModel() {
  2. // ... leave firstName and lastName unchanged ...
  3. this.fullName = ko.computed(function() {
  4. return this.firstName() + " " + this.lastName();
  5. }, this);
  6. }

并且绑定到UI的元素上,例如:

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

… 不管 firstName 还是 lastName 改变,全称fullName都会自动更新(不管谁改变,执行函数都会调用一次,不管改变成什么,他的值都会更新到UI或者其他自动计算属性上)。

… and they will be updated whenever firstName or lastName changes (your evaluator function will be called once each time any of its dependencies change, and whatever value you return will be passed on to the observers such as UI elements or other computed observables).

管理 ‘this’

新手可忽略此小节,你只需要安装上面例子中的代码模式写就行了,无需知道/关注这个this。

你可能疑惑 ko.computed 的第二个参数是做什么用的(上面的例子中我传的是 this ), 它是声明执行自动计算属性的 this 用的。 没有它,你不能引用到 this.firstName()this.lastName()。 老练的JavaScript 开发人员不觉得this怎么样,但是如果你不熟悉JavaScript,那就对它就会很陌生。(C#和Java需要不需要为set一个值为设置 this,但是JavaScript 需要,因为默认情况下他们的函数自身不是任何对象的一部分)。

In case you’re wondering what the second parameter to ko.computed is (the bit where I passed this in the preceding code), that defines the value of this when evaluating the computed observable. Without passing it in, it would not have been possible to refer to this.firstName() or this.lastName(). Experienced JavaScript coders will regard this as obvious, but if you’re still getting to know JavaScript it might seem strange. (Languages like C# and Java never expect the programmer to set a value for this, but JavaScript does, because its functions themselves aren’t part of any object by default.)

受欢迎的简化操作

如果你要将viewmodel中 this 复制到一个不同的变量上(传统上被称为 self),你可以用self在整个viewmode不用担心被重定义。例如:

There’s a popular convention for avoiding the need to track this altogether: if your viewmodel’s constructor copies a reference to this into a different variable (traditionally called self), you can then use self throughout your viewmodel and don’t have to worry about it being redefined to refer to something else. For example:

  1. function AppViewModel() {
  2. var self = this;
  3. self.firstName = ko.observable('Bob');
  4. self.lastName = ko.observable('Smith');
  5. self.fullName = ko.computed(function() {
  6. return self.firstName() + " " + self.lastName();
  7. });
  8. }

因为 self 已经被存储在变量中,不会再函数的闭包中被更改,但是它依然可用。你可以在live examples了解到更多。

Because self is captured in the function’s closure, it remains available and consistent in any nested functions, such as the ko.computed evaluator. This convention is even more useful when it comes to event handlers, as you’ll see in many of the live examples.

依赖链

理所当然,如果你想你可以创建一个自动计算属性的链。例如:

Of course, you can create whole chains of computed observables if you wish. For example, you might have:

  • 监控属性 items 表述一组列表项
  • an observable called items representing a set of items
  • 监控属性 selectedIndexes 保存着被用户选上的列表项的索引
  • another observable called selectedIndexes storing which item indexes have been ‘selected’ by the user
  • 自动计算属性返回的是 selectedItems 返回对应于选定索引的项目对象的数组
  • a computed observable called selectedItems that returns an array of item objects corresponding to the selected indexes
  • 另一个自动计算属性返回的 truefalse 依赖于 selectedItems 的各个列表项是否包含一些属性(例如,是否新的或者还未保存的)。一些UI element(像按钮的启用/禁用)的状态取决于这个值)。
  • another computed observable that returns true or false depending on whether any of selectedItems has some property (like being new or being unsaved). Some UI element, like a button, might be enabled or disabled based on this value.

然后,items 或者 selectedIndexes 的改变将会影响到所有自动计算属性的链,所有绑定这些属性的UI元素都会自动更新。多么整齐与优雅!

Then, changes to items or selectedIndexes will ripple through the chain of computed observables, which in turn updates any UI bound to them. Very tidy and elegant.

可写的自动计算属性

新手可忽略此小节,可写自动计算属性真的是太高端了,而且大部分情况下都用不到。

正如所学到的,自动计算属性是通过计算其它的监控属性而得到的。感觉是自动计算属性正常情况下应该是只读的。那么,有可能让自动计算属性支持可写么? 你只需要声明自己的callback函数然后利用写入的值再处理一下相应的逻辑即可。

As you’ve learned, computed observables have a value that is computed from other observables. In that sense, computed observables are normally read-only. What may seem surprising, then, is that it is possible to make computed observables writeable. You just need to supply your own callback function that does something sensible with written values.

你可以像使用普通的监控属性一样使用自动计算属性 – 数据双向绑定到DOM元素上,并且通过自定义的逻辑拦截所有的读和写操作。这是非常牛逼的特性并且可以在大范围内使用。

You can then use your writeable computed observable exactly like a regular observable - performing two-way data binding with DOM elements, with your own custom logic intercepting all reads and writes. This is a powerful feature with a wide range of possible uses.

例1:分解用户的输入

返回到经典的“first name + last name = full name” 例子上,你可以让事情调回来看: 让自动计算属性 fullName 可写,让用户直接输入姓名全称,然后输入的值将被解析并映射写入到基本的监控属性 firstNamelastName 上:

Going back to the classic “first name + last name = full name” example, you can turn things back-to-front: make the fullName computed observable writeable, so that the user can directly edit the full name, and their supplied value will be parsed and mapped back to the underlying firstName and lastName observables:

  1. function MyViewModel() {
  2. this.firstName = ko.observable('Planet');
  3. this.lastName = ko.observable('Earth');
  4. this.fullName = ko.computed({
  5. read: function () {
  6. return this.firstName() + " " + this.lastName();
  7. },
  8. write: function (value) {
  9. var lastSpacePos = value.lastIndexOf(" ");
  10. if (lastSpacePos > 0) { // Ignore values with no space character
  11. this.firstName(value.substring(0, lastSpacePos)); // Update "firstName"
  12. this.lastName(value.substring(lastSpacePos + 1)); // Update "lastName"
  13. }
  14. },
  15. owner: this
  16. });
  17. }
  18. ko.applyBindings(new MyViewModel());

这个例子里,写操作的callback接受写入的值,把值分离出来,分别写入到“firstName”和“lastName”上。 你可以像普通情况一样将这个view model绑定到DOM元素上,如下:

In this example, the write callback handles incoming values by splitting the incoming text into “firstName” and “lastName” components, and writing those values back to the underlying observables. You can bind this view model to your DOM in the obvious way, as follows:

  1. <p>First name: <span data-bind="text: firstName"></span></p>
  2. <p>Last name: <span data-bind="text: lastName"></span></p>
  3. <h2>Hello, <input data-bind="value: fullName"/>!</h2>

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

This is the exact opposite of the Hello World example, in that here the first and last names are not editable, but the combined full name is editable.

上面的view model演示的是通过一个简单的参数来初始化自动计算属性。你可以给下面的属性传入任何JavaScript对象:

The preceding view model code demonstrates the single parameter syntax for initialising computed observables. You can pass a JavaScript object with any of the following properties:

  • read — 必选,一个用来执行取得自动计算属性当前值的函数。
  • read — Required. A function that is used to evaluate the computed observable’s current value.
  • write — 可选,如果声明将使你的自动计算属性可写,别的代码如果这个可写功能写入新值,通过自定义逻辑将值再写入各个基础的监控属性上。
  • write — Optional. If given, makes the computed observable writeable. This is a function that receives values that other code is trying to write to your computed observable. It’s up to you to supply custom logic to handle the incoming values, typically by writing the values to some underlying observable(s).
  • owner — 可选,如果声明,它就是KO调用 readwrite 的callback时用到的 this 。查看 “管理 this” 获取更新信息。
  • owner — Optional. If given, defines the value of this whenever KO invokes your read or write callbacks. See the section “Managing this” earlier on this page for more information.

例2:Value转换器

有时候你可能需要显示一些不同格式的数据,从基础的数据转化成显示格式。比如,你存储价格为float类型,但是允许用户编辑的字段需要支持货币单位和小数点。你可以用可写的自动计算属性来实现,然后解析传入的数据到基本 float类型里:

Sometimes you might want to represent a data point on the screen in a different format from its underlying storage. For example, you might want to store a price as a raw float value, but let the user edit it with a currency symbol and fixed number of decimal places. You can use a writeable computed observable to represent the formatted price, mapping incoming values back to the underlying float value:

  1. function MyViewModel() {
  2. this.price = ko.observable(25.99);
  3. this.formattedPrice = ko.computed({
  4. read: function () {
  5. return '$' + this.price().toFixed(2);
  6. },
  7. write: function (value) {
  8. // Strip out unwanted characters, parse as float, then write the raw data back to the underlying "price" observable
  9. value = parseFloat(value.replace(/[^\.\d]/g, ""));
  10. this.price(isNaN(value) ? 0 : value); // Write to underlying storage
  11. },
  12. owner: this
  13. });
  14. }
  15. ko.applyBindings(new MyViewModel());

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

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

所以,不管用户什么时候输入新价格,输入什么格式,text box里会自动更新为带有2位小数点和货币符号的数值。这样用户可以看到你的程序有多聪明,来告诉用户只能输入2位小数,否则的话自动删除多余的位数,当然也不能输入负数,因为 write 的callback函数会自动删除负号。

Now, whenever the user enters a new price, the text box immediately updates to show it formatted with the currency symbol and two decimal places, no matter what format they entered the value in. This gives a great user experience, because the user sees how the software has understood their data entry as a price. They know they can’t enter more than two decimal places, because if they try to, the additional decimal places are immediately removed. Similarly, they can’t enter negative values, because the write callback strips off any minus sign.

例3:过滤并验证用户输入

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

Example 1 showed how a writeable computed observable can effectively filter its incoming data by choosing not to write certain values back to the underlying observables if they don’t meet some criteria. It ignored full name values that didn’t include a space.

再多走一步,你可以声明一个监控属性 isValid 来表示最后一次写入是否合法,然后根据真假值显示相应的提示信息。稍后仔细介绍,先参考如下代码:

Taking this a step further, you could also toggle an isValid flag depending on whether the latest input was satisfactory, and display a message in the UI accordingly. I’ll explain in a moment an easier way of doing validation, but first consider the following view model, which demonstrates the mechanism:

  1. function MyViewModel() {
  2. this.acceptedNumericValue = ko.observable(123);
  3. this.lastInputWasValid = ko.observable(true);
  4. this.attemptedValue = ko.computed({
  5. read: this.acceptedNumericValue,
  6. write: function (value) {
  7. if (isNaN(value))
  8. this.lastInputWasValid(false);
  9. else {
  10. this.lastInputWasValid(true);
  11. this.acceptedNumericValue(value); // Write to underlying storage
  12. }
  13. },
  14. owner: this
  15. });
  16. }
  17. ko.applyBindings(new MyViewModel());

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

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

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

Now, acceptedNumericValue will only ever contain numeric values, and any other values entered will trigger the appearance of a validation message instead of updating acceptedNumericValue.

备注:上面的例子显得杀伤力太强了,更简单的方式是在 上使用jQuery Validation和 number class。Knockout可以和jQuery Validation一起很好的使用,参考例子:grid editor。当然,上面的例子依然展示了一个如何使用自定义逻辑进行过滤和验证数据,如果验证很复杂而jQuery Validation很难使用的话,你就可以用它。

Note: For such trivial requirements as validating that an input is numeric, this technique is overkill. It would be far easier just to use jQuery Validation and its number class on the element. Knockout and jQuery Validation work together nicely, as demonstrated on the grid editor example. However, the preceding example demonstrates a more general mechanism for filtering and validating with custom logic to control what kind of user feedback appears, which may be of use if your scenario is more complex than jQuery Validation handles natively.

依赖跟踪如何工作的

新手没必要知道太清楚,但是高级开发人员可以需要知道为什么自动计算属性能够自动跟踪并且自动更新UI…

事实上,非常简单,甚至说可爱。跟踪的逻辑是这样的:

  • 当你声明一个自动计算属性的时候,KO会立即调用执行函数并且获取初始化值。
  • Whenever you declare a computed observable, KO immediately invokes its evaluator function to get its initial value. 当你的执行函数运行的时候,KO会把所有需要依赖的依赖属性(或者监控依赖属性)都记录到一个Log列表里。
  • While your evaluator function is running, KO keeps a log of any observables (or computed observables) that your evaluator reads the value of.
  • 执行函数结束以后,KO会向所有Log里需要依赖到的对象进行订阅。订阅的callback函数是重新运行你的执行函数。然后回头重新执行上面的第一步操作(并且注销不再使用的订阅)。
  • When your evaluator is finished, KO sets up subscriptions to each of the observables (or computed observables) that you’ve touched. The subscription callback is set to cause your evaluator to run again, looping the whole process back to step 1 (disposing of any old subscriptions that no longer apply).
  • 最后KO会通知上游所有订阅它的订阅者,告诉它们我已经设置了新值。
  • KO notifies any subscribers about the new value of your computed observable.

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

So, KO doesn’t just detect your dependencies the first time your evaluator runs - it redetects them every time. This means, for example, that your dependencies can vary dynamically: dependency A could determine whether you also depend on B or C. Then, you’ll only be re-evaluated when either A or your current choice of B or C changes. You don’t have to declare dependencies: they’re inferred at runtime from the code’s execution.

另外一个技巧是:一个模板输出的绑定是自动计算属性的简单实现,如果模板读取一个监控属性的值,那模板绑定就会自动变成自动计算属性依赖于那个监控属性,监控属性一旦改变,模板绑定的自动计算属性就会自动执行。嵌套的模板也是自动的:如果模板X render模板 Y,并且Y需要显示监控属性Z的值,当Z改变的时候,由于只有Y依赖它,所以只有Y这部分进行了重新绘制(render)。

The other neat trick is that declarative bindings are simply implemented as computed observables. So, if a binding reads the value of an observable, that binding becomes dependent on that observable, which causes that binding to be re-evaluated if the observable changes.

依赖监控属性(Dependent Observables)怎么不见了?

在 Knockout 2.0之前,自动计算属性(Computed Observables)被称为依赖监控属性(Dependent Observables)。2.0版本中,我们决定将ko.dependentObservable 重命名为 ko.computed。因为它的表达和类型更容易理解。但是不用担心,这个不会影响到其他代码。在运行时,ko.dependentObservable 是指向到 ko.computed — 他们俩是等价的。

Prior to Knockout 2.0, computed observables were known as dependent observables. With version 2.0, we decided to rename ko.dependentObservable to ko.computed because it is easier to explain, say, and type. But don’t worry: this won’t break any existing code. At runtime, ko.dependentObservable refers to the same function instance as ko.computed — the two are equivalent.

(c) knockoutjs.com