/ 前端工程

Knockout.js快速学习笔记

原创纯手写快速学习笔记(对官方文档的二手理解),更推荐有时间的话读官方文档

框架简介(Knockout版本:3.4.1 )

Knockout(以下简称KO)是一个MVVM(Model-View-View Model)框架,这是一种设计用户界面的设计模式,把一个复杂的UI分成三个部分:模型(Model),视图(View),视图模型(View Model)。以下解释这三个模型的含义:

  1. 模型(Model):你的应用中存储的数据。这些数据与UI是独立的。我们常向服务器发请求,从而得到数据改变模型存储的数据。

  2. 视图模型(View Model):代表了数据和UI交互的综合。比如我们正在写一个可编辑的列表,那视图模型可能就是一个对象,这个对象包含了列表中的项的数据和增加或者编辑这些数据的方法,这是一个抽象的模型。

  3. 视图(View):一个可见的,交互的用户界面。它展现了视图模型的状态和信息并随着视图模型的变化而变化。

例子:
VM:
var myViewModel = {
personName: 'Bob',
personAge: 123
};
V:
The name is < span data-bind="text: personName">
激活这个MVVM:
ko.applyBindings(myViewModel);

注意:
这里的ko.applyBindings()方法,它是这样使用的:

第一个参数:传递绑定的视图模型。

第二个参数:决定DOM中哪部分发生绑定的解析。要求是一个元素对象。如果不传入这个参数,那将把视图模型作用域整个DOM上。


以下介绍KO的特性和使用方法:

数据观测

----ko.observable()

之前我们把视图模型中的数据展现了出来,但是当视图模型中的数据更新的时候,我们希望视图也跟着自动更新怎么办?这就需要监测视图模型的数据更新了。我们用这种方法(ko.observable())调用KO的自动更新机制:

var myViewModel = {
    personName: ko.observable('Bob'),
    personAge: ko.observable(123)
};

我们把这种方式称作属性向观测器发起了订阅,当KO的观测器观测到数据的变化时,就会把变化发布给属性,给属性赋予新的值。

这时personName和personAge就成了可被观测的属性,我们可以用以下方法读写可被观测的属性:

  • 读:myViewModel.personName() //返回'Bob'

  • 写:myViewModel.personName('name')

  • 链式写:myViewModel.personName('name').personAge(10)

----instance.prop.subscribe()

如果需要手动订阅某个可观测属性的变化信息,并在执行指定的回调函数。那么可以对可观测属性使用以下方法:myViewModel.personName.subscribe(callback, target, event)。其中callback是一个回调函数,它会根据所订阅的事件被传入相应的参数,target可以显式指定callback中的this指向,event为订阅的变化事件的名称,默认为"change"。用法实例:

myViewModel.personName.subscribe(function(newValue) {
  alert("The person's new name is " + newValue);
});

如果想取消手动的订阅,首先把订阅时的返回值存储在一个变量里,之后调用这个变量的dispose()方法即可。用法实例:

var subscription = myViewModel.personName.subscribe(callback, target, event)
// 取消手动订阅
subscription.dispose()

如果想订阅属性变化前的信息,对变化前的信息执行回调,那么可以将subscribe()的第三参数传为"beforeChange",这样第一参数callback默认会接收到oldValue。

----ko.observableArray()

如果一个被观测属性是一个数组,那么我们最好把ko.observable()方法换成ko.observableArray()方法,这样更有利于才做可被观测的数组。本质上这个属性仍然是ko.observable方法生成的,但是添加了一些附加方法和属性。详细的方法和属性请查阅文档。

---ko.computed(fn)

如果需要得到一个基于已有的可被观测属性计算得出的属性,那可以使用ko.computed()方法得到一个计算属性,这个计算属性会在所依赖的属性更新时,自动更新自己的值。使用示例:

function AppViewModel() {
  this.firstName = ko.observale('Bob')
  this.lastName = ko.observable('Smith')
  // 以下属性就是一个计算属性
  this.fullName = ko.computed(function () {
    return this.firstName() + ' ' + this.lastName()
  }, this) // 此处的this用于指明ko.computed的'第一参数函数'内部的this指向
}

如果只是简单的计算并返回可被观测属性就得到计算属性,那么使用ko.pureComputed()方法会比较好,可以提升性能。例如上面的例子,其实更适合用ko.pureComputed()。

高级扩展:计算属性生成时可以传入一个包含read, write, owner属性的对象,得到一个可写的计算属性。一般用不到。

绑定

绑定是开发项目时使用到的核心功能,本质上,绑定就是把一些KO预设的内部"指令"(下方就将介绍),根据需要写在HTML标签内,有点相当于"把JS写进HTML"中。之后KO会去自动执行写在HTML标签中的指令,以达到我们预期的效果。

指令:我们可以理解成KO预设的一些功能,给标签绑定不同的指令就是在标签上调用不同的KO功能。

简单示例:

Today's message is: <span data-bind="text: myMessage"></span>

指令绑定就是把指令绑定在元素标签内,写成元素的属性。语法为:data-bind="name: value",其中data-bind是KO预设的,必须写成这个,KO才知道这个标签绑定了指令,它由两部分组成--name和value。这个示例中,绑定的指令name是text,value是myMessage。text指令表示的功能是向绑定的标签内填充文本,文本的值是myMessage,这个myMessage从之前介绍的ko.applyBindings()方法所传入的对象中获取(实际情况其实更复杂一些,这个myMessage的取值是从**"绑定上下文"**中去取值的,下面有专门介绍"绑定上下文",如果想先了解可以先去看一下,不过推荐按照本文的行文顺序看下去)。

理解了什么是指令绑定基本就完成了对KO的核心功能的理解了,接下来我们只需要了解有哪些指令以及各个指令怎么用就可以快速上手使用KO了。

绑定的指令主要分为以下三种:

1.控制文本和外观的指令,比如控制标签内的text, classs, style等。

2.控制文档流的指令,比如控制DOM节点上是否需要生成某个节点,根据视图模型(View Model)里的数组自动循环生成DOM节点等。

3.表单控件相关的指令,比如控制输入框的值,是否选中,是否提交等。

现在就一一介绍这三种指令都有哪些以及具体的指令功能,使用方法不做介绍,可直接查看官方文档

----控制文本和外观的指令

*visible:控制绑定元素的显示隐藏,如果绑定的值转换成布尔值为真则显示,否则display: none。(如果绑定一个函数则会执行并取返回值)

*text:控制绑定元素内部的文本内容,接收一个字符串的值作为绑定值,如果不是字符串会调用绑定的值的toString()方法。(如果绑定一个函数则会执行并取返回值)

*html:控制元素内部的HTML文本,换句话说,通过这个指令可以直接向标签内写入HTML。传参要求为字符串,若非字符串将调用toString()方法转换成字符串。

*css:控制元素的class属性,传入的值要求为字符串或者对象(或对象字面量),为字符串时将直接应用到元素的class,为对象时,对象的key为class,value为true则添加class,否则不添加。(如果绑定一个函数则会执行并取返回值)

*style:控制元素的内联式样式,要求传入一个对象,对象的key为驼峰形式的样式名,值为样式的值。(如果绑定一个函数则会执行并取返回值)

*attr:控制元素的属性,要求传入一个对象,对象的key为属性名,值为属性的值。

----控制文档流的指令

*foreach:通过遍历数组数据生成DOM结构。将传递的数组的每项数据应用到绑定元素的内部内容里,并根据传入的数据重复生成相同的子节点。这个指令灵活多变,本文下方的补充小节会做详细说明,更推荐看文章头部给出的官方文档。

*if:控制绑定元素内的DOM节点是否生成,与visible指令类似,但是visible指令是通过CSS来控制节点的显隐,而if直接决定是否产生子节点。要求传入一个布尔型的值,为真则生成子节点,否则不生成子节点。可以和foreach一样,在一个KO虚拟元素上进行指令绑定。

*ifnot:作用和if指令一样,只是传入的布尔值为false时生成子节点,否则不生成。和if: !value等价。

*with:在绑定元素的内部创建新的绑定上下文(下方有介绍)。要求传入一个对象作为内部的绑定上下文。

*component:在绑定元素内部注入指定的组件,并可传入指定的参数给组件。组件是KO的另一个功能模块,将在下方补充说明里介绍。使用组件,可以提高代码的复用性。

----表单控件相关的指令

*click:当绑定元素被点击时,执行传入的回调函数。回调函数可以是视图模型里的某个方法,也可以是某个对象的方法,如果是某个对象的方法,就这么调用:someObj.someFunc。值得注意的是,在回调函数中,KO会传入第一个参数为绑定上下文中的$data,第二个参数为事件对象。如果还想传入更多参数,可以使用函数字面量或者bind()方法,具体请参考click绑定。另外,click绑定默认是阻止点击事件的默认行为的,比如点击一个绑定了click指令的< a>标签,并不会发生跳转,想要不阻止默认行为,在回调函数里return true;即可。click绑定默认也是发生事件冒泡的,如果想阻止冒泡,那么应该这样传递值data-bind="click: myHandler, clickBubble: false"

*event:为指定事件绑定回调函数。要求传入一个对象(对象字面量更好),对象的key为事件名称,value为对应的回调函数。如果想传递更多的参数,用函数字面量是一个不错的办法。此外还可以类似myFunc.bind($root)。事件绑定

*submit:为< form>标签绑定submit事件的回调函数。默认传入form元素给回调函数。

*enable:控制绑定的标签是否有效。这个指令在< input>, < select>, < textarea>标签上可以产生作用。需要传入一个布尔值或者一个表达式,如果不是布尔值,则会转换成布尔值。

*disable:与enable类似,只是逻辑值相反。

*value:控制绑定元素的value值,在< input>, < textarea>, < select>标签上有作用。需要传入一个字符串,如果不是字符串,将会转换成字符串。此外还可以传递一些其它的配置项,比如valueUpdate的回调函数等。详见value绑定

*textInput:与value指令类似,但是textInput提供双向的数据绑定,意思即value指令只会将绑定的值付给绑定元素的value,但是当输入框的值发生变化时,视图模型的值不能同步更新。这时就需要用textInput指令,实现数据的双向绑定。

*hasFocus:控制绑定元素的focus状态。为true则绑定元素处于focus状态。这也是一个双向绑定的指令。

*checked:控制绑定元素的checked状态,对于radio, checkbox有效。传入的参数如果为布尔值,则true为选中状态,false不选中。如果传入一个数组,则根据当前绑定元素的value和数组进行匹配,如果存在于数组中则为选中状态,否则就是没选中。还可以参考文档看其它配置项。

*options:绑定在< select>标签上,生成select标签内的options,要求传入一个数组。还可以参考文档看其它配置项。

*selectedOptions:绑定在< select>标签上,决定被选中的option。要求传入一个数组。

*uniqueName:一般不会用到,为绑定的标签添加独一无二的name属性,传入的值为true就添加。

渲染模版

绑定上下文

绑定上下文 :指令绑定在上面已经说了,那么这里就涉及到一个重要的概念--绑定上下文。

绑定上下文其实可以理解成KO不同运行时刻的视图模型,我们在调用ko.applyBindings(ViewModel)方法时,所传递的ViewModel就是最顶层的视图模型,但当我们在绑定一些特殊的指令(比如foreach)时,在绑定元素的内部会有新的视图模型被应用。

如果还是对绑定上下文的概念有些模糊,那我们来了解一下绑定上下文的意义:绑定上下文的意义在于:我们在绑定指令时,一定会传一个value值给这个指令,KO在解析这条指令时,需要有一个取值的环境来取值,这个环境就是**"绑定上下文"**,绑定上下文除了含有我们视图模型中显式定义了的数据,还有一些内部变量供指令调用,比如$parent, $parents, $data等,这些变量将在下方挑一些重要的进行介绍,详细的介绍请看官方文档的Binding Context部分。

在介绍KO内置给绑定上下文提供的变量之前,先用一个实际的示例来说明绑定上下文在解析绑定的过程中发生的变化(注意看注释)。

HTML:

<h4>People</h4>
<ul data-bind="foreach: people"> <!-- 在执行这条指令时,此处的绑定上下文是myViewModel,所以取myViewModel里的people这个值 -->
  <!-- 因为遇到了foreach指令,所以从这里开始,将进入新的绑定上下文,直至foreach绑定的标签ul结束,另外因为people的数组长度为3,所以要遍历生成三次才会结束  -->
  <li>
    Name at position <span data-bind="text: $index"> </span>:  <!-- foreach第一次遍历生成内部的这些节点时,$index为0,之后递增,$index是KO为绑定上下文内部添加的变量 -->
    <span data-bind="text: name"> </span> <!-- 第一次绑定上下文的name为Bert,其后分别为Charles、Denis -->
    <a href="#" data-bind="click: $parent.removePerson">Remove</a> <!-- $parent是KO内部为绑定上下文添加的变量,指向上一层的视图模型,此处即myViewModel -->
  </li>
  <!-- 遍历生成三次后结束 -->
</ul> <!-- 在此处之后,绑定上下文恢复为之前的myViewModel -->
<button data-bind="click: addPerson">Add</button> <!-- 从当前绑定上下文myViewModel中取得addPerson方法 -->

Javascript:

function AppViewModel() {
  var self = this;
  self.people = ko.observableArray([
    { name: 'Bert' },
    { name: 'Charles' },
    { name: 'Denise' }
  ]);
  self.addPerson = function() {
    self.people.push({ name: "New at " + new Date() });
  };
  self.removePerson = function() {
    self.people.remove(this);
  }
}
var myViewModel = new AppViewModel()
ko.applyBindings(myViewModel);

绑定上下文中的内部变量:

*$parent:即当前绑定上下文的上一层视图模型。对于最顶层的视图模型来说,比如上面例子中的myViewModel,$parent为undefined。

*$parents:所有祖先视图模型构成的数组,$parents[0]为父视图模型(等同于$parent),$parents[1]为"爷爷视图模型",依此类推……直至到达最顶层的视图模型。

*$root:最顶层的视图模型,就是传给ko.applyBindings()方法的参数。等同于$parents[$parents.length - 1]

*$data:当前视图模型的数据,比如上面示例中的foreach循环中绑定的name,也可以用$data.name取到。

*$component:组件模版内调用,得到组件模版对应的视图模型。

绑定上下文中还有许多其它的内部变量,请参看binding-context 获得详细说明。

补充

----foreach指令

上面的指令中,foreach值得深入探究一下,因为它的使用方法比较灵活,并且比较重要。

首先,这个指令的传入参数为一个数组或一个对象字面量。

当传入一个数组时,指令会直接遍历数组然后对每条数据进行对应的解析生成然后添加到父元素中。

当传入一个对象字面量时,对象字面量的data属性会被遍历,配置项属性可以直接看foreach绑定,比如有as, afterRender等属性。

*如果所遍历数组的每个值都是对象的话,那对象的属性和值会马上被写入遍历时产生的绑定上下文中,可以在foreach指令内部直接取到对象的值。

*如果所遍历的数组的值不是对象,那直接在foreach的内部用$data去取。

一些典型的用法:

*data-bind="foreach: { data: ary, as: description}":这里ary是绑定上下文中的一个数组,我们用对象字面量的形式传值给foreach指令,之后配置的as,其实和$data等同,是一个别名,让我们在foreach的内部以一个更具有描述性的名称取得视图模型中的数据。

*不使用容器元素直接调用foreach指令:之前我们都是将foreach指令写在一个标签中绑定调用。可是有一些场景:比如在标签内部除了foreach循环生成的内容,我们还想要一些特殊的内容,这时候如果在标签上绑定foreach就实现不了了,此时我们就需要不用容器元素,直接调用foreach指令。使用方法如下:

<ul>
  <li class="header">Header item</li>
  <!-- ko foreach: myItems -->
    <li>Item <span data-bind="text: $data"></span></li>
  <!-- /ko -->
</ul>

<script type="text/javascript">
  ko.applyBindings({
    myItems: [ 'A', 'B', 'C' ]
  });
</script>

此处的是KO预设的虚拟元素,并不会真正渲染,但我们可以在虚拟元素上绑定想执行的指令。

----component组件:

使用组件的好处有很多,比如化繁为简,复用性提高,可以异步加载, 可以组合使用,能自我管理自己的视图模型等。

*ko.components.register():这个方法用于声明一个组件,需要传入两个参数,第一个参数为声明的组件的名称。第二个参数为组件的配置对象。现在看一个实际的例子:

ko.components.register('like-widget', {
  viewModel: function(params) {
    // Data: value is either null, 'like', or 'dislike'
    this.chosenValue = params.value;

    // Behaviors
    this.like = function() { this.chosenValue('like'); }.bind(this);
    this.dislike = function() { this.chosenValue('dislike'); }.bind(this);
  },
  template:
    '<div class="like-or-dislike" data-bind="visible: !chosenValue()">\
        <button data-bind="click: like">Like it</button>\
        <button data-bind="click: dislike">Dislike it</button>\
    </div>\
    <div class="result" data-bind="visible: chosenValue">\
        You <strong data-bind="text: chosenValue"></strong> it\
    </div>'
});

以上就声明了一个叫做< like-widget>的组件。viewModel属性配置组件的视图模型,template属性配置组件的视图。其中viewModel函数的params参数将在< like-widget>组件被调用时由我们自己手动传入。调用这个组件的方法:

<ul data-bind="foreach: products">
  <li class="product">
    <strong data-bind="text: name"></strong>
    <like-widget params="value: userRating"></like-widget>
  </li>
</ul>

可以看到,对于注册的调用,直接将组件名当作标签使用,并传入params参数就可以了。更好的做法是我们可以用模块加载器(比如require.js)定义和加载模块。详细方法参考模块总览

----复选框全选/全不选/未全选控制:可写计算属性

利用可写计算属性,可以简明易懂地实现复选框全选和未全选的控制。代码如下:

HTML:

<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>

JS:

function MyViewModel() {
  this.produce = [ 'Apple', 'Banana', 'Celery', 'Corn', 'Orange', 'Spinach' ];
  this.selectedProduce = ko.observableArray([ 'Corn', 'Orange' ]);
  this.selectedAllProduce = ko.pureComputed({
    read: function () {
      return this.selectedProduce().length === this.produce.length;
    },
    write: function (value) {
      this.selectedProduce(value ? this.produce.slice(0) : []); // 写入时,判断是否是勾选了全部选中,如果全选,则把数据数组写入到选中数组中;全不选,则清空选中数组。
    },
    owner: this
  });
}
ko.applyBindings(new MyViewModel());

主要由三部分实现:1.原始数据数组(示例中为produce);2.选中项数组(示例中为selectedProduce);3.可读计算属性,计算是否全部选中(示例中为selectedAllProduce)。对于这个可读计算属性,read为取值函数,write为写值函数,owner指定这两个函数内部的this指向。