计算属性

konckout.js的API文档,写的极为详细和生动,透过MVVM的运作原理,开发时只需专注于定义ViewModel逻辑,不需耗费心力处理TextBox、Select的onchange、onclick等互动细节,就能达到UI元素与数据天人合一的境界。这一系列的 konckout 学习笔记,将逐一探讨knockout.js在常见网页情境上的应用。

Knockout.js随手记(1) 开篇已经示范过最简单的应用,为<input>与<span>加上data-bind宣告,透过ko.applyBindings()绑定到定义好的ViewModel上,当input改变输入内容,会立即反应在span。然而,有些场合数据需经加工才能使用,例如: 指定日期格式,将数字相加... 等等,此时ko.computed()便派上用场。

使用ko.computed()时,最简单的做法是直接传入function,在其中引用其他的ViewModel属性处理运算后传回结果;因knockout具备强大的依赖关系追踪能力,能记下你引用了哪些属性,一旦被引用的属性来源改变,便会自动调用ko.computed()计算新值。

范例1

首先我们创建个ViewModel, 然后 将使用计算属性将结果返回给fullName.

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

现在我们做的事情就是绑定这些值

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

我们来查看下运行效果:

注意:由于this在不同的范围内又不同的含义,往往为了避免混淆,我们采用一项流行的约定,就是把这个this指向另一个习惯性的变量(self),我们的代码可以修改为如下:

<body>
    <p>
        First name:
        <input data-bind="value: firstName" />
    </p>
    <p>
        Last name:
        <input data-bind="value: lastName" />
    </p>
    <h2>
        Hello,
        <span data-bind="text: fullName" />
        !
    </h2>
    <script type="text/javascript">
        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();
            },
            this);
        }
        ko.applyBindings(new AppViewModel());
    </script>
</body>  

范例2

可能你和我一样在想,既然knockout支持依赖性追踪特性,那么,我可以通过更改fullName的值去动态修改first Name和 Last Name吗?

那么我们做的工作就是分解FullName的输入值。

   <p>First name: <input data-bind="value: firstName"/></p>
     <p>Last name: <input data-bind="value: lastName"/></p>
     <h2>Hello, <input data-bind="value: fullName"/>!</h2>
    <script  type="text/javascript">
        function MyViewModel() {
            var self = this;
            self.firstName = ko.observable('Planet');
            self.lastName = ko.observable('Earth');

            self.fullName = ko.computed({
                read: function () {
                    return self.firstName() + " " + self.lastName();
                },
                write: function (value) {
                    var lastSpacePos = value.lastIndexOf(" ");
                    if (lastSpacePos > 0) { // Ignore values with no space character
                        self.firstName(value.substring(0, lastSpacePos)); // Update "firstName"
                        self.lastName(value.substring(lastSpacePos + 1)); // Update "lastName"
                    }
                },
                owner: self
            });
        }

        ko.applyBindings(new MyViewModel());
    </script> 

代码很简单,可以说也是最常见的js截取字符串的应用,我们只要注意其中的三个代理功能:read,write,owner就可以,实现的效果如下:

范例3

这例子其实算是对范例2的一个复习,主要功能是提供金额格式的自动转换(包括精度和格式)已经垃圾字符的过滤

<p>
    Enter bid price:
    <input data-bind="value: formattedPrice" />
</p>
<br/>
<script type="text/javascript">
    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());
</script>

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

我们来查看下运行效果:

范例4 过滤并验证用户输入

  <p>Enter a numeric value: <input data-bind="value: attemptedValue"/>
      <span data-bind="visible:lastInputWasValid()">验证通过!</span>
  </p>
  <div data-bind="visible: !lastInputWasValid()">这不是一个合法的数字!</div>

    <script  type="text/javascript">
        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());
    </script> 

运行效果:

依赖跟踪是如何工作的

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

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

1. 当你声明一个依赖监控属性的时候,KO会立即调用执行函数并且获取初始化值。

2. 当你的执行函数运行的时候,KO会把所有需要依赖的依赖属性(或者监控依赖属性)都记录到一个Log列表里。

执行函数结束以后,KO会向所有Log里需要依赖到的对象进行订阅。订阅的callback函数重新运行你的执行函数。然后回头重新执行上面的第一步操作(并且注销不再使用的订阅)。

3.最后KO会通知上游所有订阅它的订阅者,告诉它们我已经设置了新值。

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

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

范例5

knocut自动依赖性跟踪通常不正是您想要。但你有时可能需要控制观测值将更新计算观察到的,尤其是如果计算可观察执行某种操作,如一个Ajax请求。peek函数,可以让你访问到监控属性或计算属性,而无需创建一个依赖。

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

其中selectItem属性使用了Peek函数,所以计算属性会随时监控和更新PageIndex的值,但它忽略更改selectItem。