Knockout开发中文API系列3–使用计算属性

计算属性

如果你已经有了一个监控属性 firstNamelastName,如果你想显示全名该怎么做呢?这个时候你就可以通过计算属性来实现,这个方法依赖于一个或多个监控属性,如果任何依赖对象发生改变他们就会跟着改变。

例如,下面的 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.   
  4.     this.fullName = ko.computed(function() {   
  5.         return this.firstName() + " " + this.lastName();   
  6.     }, this);   
  7. }  

下面你就可以将它绑定到UI对象上了,如:

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

firstName或者lastName 变化,它将会随时更新(当依赖关系发生变化,你的计算函数就会被调用,并且它的值都会更新到UI对象或其他的依赖属性上去)。

管理"this"

初学者不妨可以调过这一节—只要你遵循示例规范你的编码模式,你不需要关系它。

你可能想知道 ko.computed的第二个参数是什么(前面的代码中我们使用到了this),当依赖属性时定义了this的值,没有传递它进去,你不可能得到this.firstName()或者 this.lastName().有经验的JavaScript开发人员觉得this没什么的,但如果你不熟悉JavaScript,你就会觉得这样写很奇怪。(像C#和Java这类语言不需要开发人员给this赋值,但JavaScript需要,因为在默认情况下,它的函数本身并不是任何对象的一部分)。

一种简化的流行惯例

当你需要全程跟踪this 时,下面的写法是一种很流行的惯例:如果你将你的 viewmodel's结构this作为一个变量复制一份(传统称之为self),在以后你可以使用self来代表viewmodel而不必担心它重定义或指别的东西。例如:

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

由于self 在闭合的方法内部也是可以捕获到的,所以在任何任何嵌套函数当中,它仍然可用并保持一致性。 如ko.computed求值,当涉及到事件处理时它依然显得很有用。你可以通过在线实例了解更多。

依赖链工作

当然,如果你愿意,你可以创建一个完整的依赖属性链。例如,你可能有:

  • 1、用items 监控属性来代表一组items项
  • 2、另外一个selectedIndexes 监控属性来用户已选择项目的索引
  • 3、一个selectedItems 依赖属性来返回一组用户已选择的Item对象
  • 4、另外一个依赖属性来返回 true 或者false ,来表示selectedItems中是否包含一些属性(如新的或尚未保存的)。一些UI元素,比如一个按钮可以基于此值来控制其启用或禁止。

然后,当改变 items 或者selectedIndexes都会影响到所有的依赖属性链,然后依次更新到绑定了这些项的UI。非常的整洁和优雅。

 

可写的计算属性

初学者可以调过这一小节,可写的计算属性相对来说比较高级,在大多数情况下也是没有必要的。

正如你上面所学的,计算属性是通过计算其他监控属性而得到的一个值。从这个意义上说,计算属性通常情况下是只读的,你可能会比较惊讶,怎么可能让计算属性变的可写。你仅仅只需要提供一个回调函数来实现值的写入。

然后你可以把这个可写的计算属性当成一个普通的监控属性来使用,通过你自定义的逻辑来实现它的读和写。这个强大的功能可以拓宽我们对KO的使用范围,你可以通过链式语法在一个View Model上传入多个监控属性或者计算属性,例如: myViewModel.fullName('Joe Smith').age(50)

示例一:分解用户输入

返回到前面经典的“first name + last name = full name” 示例,你可以在返回全名之前,使fullName 计算属性变得可写,所以用户可以直接编辑全名,而程序可以将其输入的值进行解析并映射到底层绑定到firstName 和lastName监控属性上。

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

在这个例子当中,write 回调事件来处理用户输入的值将其分解成“firstName”和“lastName”两个部分,并将这些值返回到底层监控属性上。你可以按照如下的方式将你的view model绑定到你的DOM对象上:

  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示例是完全不同的,因为在这里“firstName”和“lastName”是不可编辑而全名确实可编辑的。
前面的 view model代码只用到了一个参数进行初始化计算属性,你可以点击 "computed observable reference"查看完整的可选参数选项。

示例二:值转换

有时你可能需要对底层存储的一个数据进行简单的转换之后显示给用户。例如:你可能需要存储浮点值来表示价格,但想让用户价格单位符号和固定的小数数位都一样。你可以使用一个可写的计算属性来完成价格转换,用户传入一个浮点型值自动映射成想要的格式。

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

这样就可以简单的将价格值绑定到text文本上

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

现在,任何时候用户输入一个新价格,文本框中会立即更新成带有单位符号和固定小数位格式的价格数字,而无论是输入什么格式的值。这是一种很好的用户体验,因为用户能够看到软件能够很好理解他们的输入并将其转换成价格。他们知道他们不能输入两位以上的小数位,如果他们输入,额外的小数位会被立即删掉,同样,他们也不能输入负值,因为write回调方法会忽略任何的减号。

示例三:筛选和验证用户输入

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

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

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

按照如下格式绑定DOM元素:

  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值之前,任何其他输入的值将触发显示验证消息。

备注:上面的例子中,对于输入值进行数字验证显得有些琐碎,这样做显得得不偿失,更简单的做法是在 <input>  元素上使用Jquery验证它是否是数值类型。Knockout 和jQuery 可以很好的在一起工作,可以 grid editor 参考这个例子。当然,上面的例子依然展示了一个如何使用自定义逻辑进行过滤和验证数据,如果验证很复杂而jQuery Validation又无法很好实现的时候,就可以这样使用它。

 

 

依赖跟踪是如何工作的

初学者可以不必知道这一点,但是高级开发人员可以通过这节来了解依赖监控属性可以通过KO自动跟踪并被更新到UI上。

事实上它是很简单的,甚至简单的有点可爱,跟踪算法是这样的:

  • 1、 当你声明一个依赖属性时,KO会立即调用求值算法得到其初始值;
  • 2、 当你的计算函数运行的时候,KO会把监控属性通过计算得到的值都记录在一个Log中;
  • 3、 当你的计算结束的时候,KO会订阅能够访问的监控属性或依赖属性,订阅的回调函数是重新运行你的计算函数,循环整个过程,回到步骤1(并且注销不再使用的订阅);
  • 4、 KO会通知所有的订阅者,你的依赖属性已经被设置了新值。

所以说,KO并不仅仅是在第一次执行计算函数时检测你的依赖项,它每次都会检测。这意味着,你的依赖是可以动态的,举例来说:依赖A能决定你是否也依赖于B或C,这时候只有当A或者你选择的B或者C发生变化时计算函数才能运行。你不需要定义依赖关系:在代码运行时会自动检测到。

另外声明绑定是依赖属性的一种简单优美的实现。所以,一个绑定是读取监控属性的值,这个绑定变成这个监控属性的依赖,当监控属性发生改变的时候,会引起这个绑定被重新计算。

使用peek控制依赖

Knockout的自动依赖跟踪通常不是你想要的,但是你有时可能需要控制那些会更新依赖属性值的监控属性,特别是依赖属性会执行某些操作时,比如一个Ajax请求。peek方法可以帮助你在不需要创建依赖的情况下去控制一个监控属性或者依赖属性。

在下面的例子中,依赖属性通过Ajax方法和其他两个监控属性参数来重新加载一个名为currentPageData 的监控属性。当pageIndex发生变化时,依赖属性会被更新,但会忽略掉selectedItem 的变化,因为它是通过peek方法控制的。在这种情况下,用户可能希望仅仅在数据被加载时才使用selectedItem 的当前值用于追踪。

Code
  1. ko.computed(function() {   
  2.     var params = {   
  3.         page: this.pageIndex(),   
  4.         selected: this.selectedItem.peek()   
  5.     };   
  6.     $.getJSON('/Some/Json/Service', params, this.currentPageData);   
  7. }, this);  

注意:如果你不想一个依赖属性过于频繁的更新,你可以参考throttle扩展

注意:为什么循环依赖是没有意义的

依赖属性是一个虚设的监控属性输入到一个单一的监控属性输出之间的映射。因此,它并不会包含在你的依赖链循环中。循环不类似于递归,它们类似于各自含有计算方法的两个电子表格的单元格。这将导致一个无限的运算循环。

如果你的依赖图当中含有一个循环的话,Knockout是如何处理的呢?可以通过执行下面的规则来避免无限循环:Knockout当它已经运算过它就不会再重新运算。这个不太可能影响你的代码。在下面两种有关情况下:当两个依赖属性互相依赖(可能其中一个或两个都使用了deferEvaluation 选项),或者一个依赖属性写到另外一个含有依赖关系的依赖属性上(无论是直接或间接的通过依赖链)。如果你想使用这些模式并且想完全避免循环依赖,你可以使用peek 方法来实现上述功能。

确定一个属性是依赖属性

在某些情况下,通过编程的方式来处理一个依赖属性是非常有用的方法。Knockout 提供了一个很实用的方法:ko.isComputed。例如,你可能想要从发给服务器的数据中排除依赖属性。

Code
  1. for (var prop in myObject) {   
  2.   if (myObject.hasOwnProperty(prop) && !ko.isComputed(myObject[prop])) {   
  3.       result[prop] = myObject[prop];   
  4.   }   
  5. }  

此外,Knockout提供了类似的方法用来操作监控属性或者依赖属性:

ko.isObservable-当是observables、observableArrays或者 computed observables时返回true

ko.isWriteableObservable-当是observables、observableArrays或者可写的 computed observables时返回true

依赖属性参考

一个依赖属性可以通过以下方式实现:

1、 ko.computed( evaluator [, targetObject, options] ) 这是用于创建依赖属性的一种最常见的方式。

  • evaluator --用于计算一个依赖属性当值的方法
  • targetObject --如果给定,当KO调用回调函数时,定义一个值表示this。参阅管理”this”来了解更多信息。
  • options --依赖属性的参数对象。可以参看下面详细的清单。

2、 ko.computed( options ) --单一参数方式接受一个JavaScript对象或者以下任意属性来创建一个依赖属性

  • read --必需参数,传入方法。用于运算当前依赖属性当前值的方法。
  • write –可选参数,传入方法。如果给定,将会使依赖属性可写这个方法接收一个外部的值来写入到依赖属性中。它通常是使用你自己定义的逻辑来处理传入的值,通常将值写入到相关的监控属性中。
  • owner –可选参数,传入对象。传入的对象作为this的关键字在KO调用readwrite方法使用。
  • deferEvaluation –可选参数,传入ture或者false。如果设置为true,则依赖属性的值直到有实际访问它之前它的值是不会重新计算的。默认情况下,依赖属性的值在创建过程中就已经初始化了。
  • disposeWhen –可选参数,传入方法。如果给出,该传入方法将会在每一次运算结束之后被调用来释放依赖属性。真正的结果就是触发依赖属性的disposal方法。
  • disposeWhenNodeIsRemoved –可选参数,传入方法。如果给出,当指定的DOM元素被KO删除的时候依赖属性的disposal方法会被触发。当元素的绑定被模版或者控制流程绑定方法移除的时候,此功能是用来释放依赖属性。

依赖属性提供了以下方法:

  • dispose()–释放依赖属性,清除所有的依赖订阅。此方法非常有用,当你想停止一个依赖属性以避免其更新或者清除一个内存中的依赖属性而那些存在依赖关系的监控值是不会被清除的。
  • extend(extenders)–用于扩展依赖属性。
  • getDependenciesCount()–返回依赖属性当前依赖关系数量。
  • getSubscriptionsCount()–返回依赖属性当前订阅数量(无论是其他的依赖属性或手动订阅)。
  • isActive ()–返回依赖属性在以后是否会被更新,一个依赖属性如果没有依赖关系是无效的。
  • peek ()–返回当前依赖属性的值而无需创建依赖关系(可以参考peek)。
  • subscribe( callback [,callbackTarget, event] )–注册一个手动订阅来通知依赖属性的变化。

依赖属性发生了什么

在Knockout2.0之前,计算属性被称之为依赖属性,在2.0版本中,我们决定重命名ko.dependentObservableko.computed,因为它在读、解释和类型上更简单。但你不用担心:这不会破坏当前所有的代码。在实际使用中,ko.dependentObservable 和 ko.computed 是等价的。

你可能感兴趣的:(knockout)