最近马上要开始一个新项目的研发,作为第一次mvvm应用的尝试,我决定使用knockoutjs框架。作为学习的开始就从官网的Document翻译开始吧,这样会增加印象并加入自己的思考,说是翻译也并不是纯粹的翻译,会加入自己对知识点的思考以及自己的尝试,在系列最后也希望用一个应用案例作为结尾。希望自己能坚持下来并有所收获,理解不对的地方大家也指出来避免我”误入歧途“。也并不会翻译所有的内容,我会根据自己的经验选择最能反映它使用和精髓的部分。当前版本为3.4
好了,闲言少叙,正篇开始。
===================================华丽的分割线============================================
knocket主要围绕由以下三个核心特征组成:
- Observables(观察器)与dependency tracking(依赖追踪)
- Declarative bindings(声明式绑定)
- Templating(模板)
MVVM and View Models
Model-View-View Model (MVVM) 是一种创建用户界面或用户接口的设计模式。你可以通过将UI拆分成下面三个部分从而将复杂的UI简洁化,清晰化:
- Model(数据模型): 应用程序存储数据。模型包括了你应用程序域中与业务相关的数据以及这些数据对应的操作(例如 一个银行账户模型具有转账的功能)并且它跟UI是相互独立 的。 当时用KO的时候,通常这类模型是通过ajax从服务器端进行获取的。
- ViewModel(视图模型): 服务于数据模型,为数据模型在UI上的展现及UI上的操作进行包装服务。 例如, 如果你要在UI上做一个列表展现, 你的视图模型将会是包含一个数据集合的对象, 并提供一些添加和删除数据的相关方法。需要注意的是视图模型并不是UI本身,它并不包括任何UI元素,比如按钮啊,标签啊样式之类。它也不是持久化的数据对象,它只是为数据模型临时保存一些用户正在处理的数据。当我们在使用KO的时候,它无非就是一些纯粹的javascript对象而已。
- A view(视图): 一个可视,可交互的真正的UI展现,他展现着视图模型的当前状态。它所展现的信息来自于视图模型,并且向视图模型发送命令执行动作 (例如: 当用户点击按钮的时候,视图向视图模型发送命令,视图模型进行真正的操作),并且当视图模型属性发生变化的时候,视图会自动进行展现的更新。当时用KO的时候,你的视图中的Html元素内容可以通过声明式绑定与视图模型进行连接从而决定UI如何展现。另外,你也可以通过使用模板的方式来使用视图模型中的数据生成UI中的Html内容。(模板后面会提到)
好了,让我们来举个很小的例子来看看上面说的视图与视图模型在KO中是如何协作的。创建一个视图模型对象非常容易,随意声明一个javascript对象我们就可以将它作为视图模型对象。例如:
//创建一个视图模型对象
var myViewModel = { personName: 'Bob', personAge: 123 };
ko.applyBindings(myViewModel);//ko是knockoutjs中的全局对象,这句话的意思是将数据模型对象与UI中所有有data-bind的属性进行声明绑定的元素进行连接
然后我们就可以创建一个非常简单的视图来展现上面的视图对象。还记得吗?他们使用声明式绑定来进行连接。下面的视图用来展现视图模型对象中的personName数据。
The name is <span data-bind="text: personName"></span>
好了现在如果运行页面的话将会显示如下运行结果:
通过上面的例子我们可以看到,在UI上赋值我们并没有像jquery一样通过js来控制,而是使用声明绑定的方式在UI元素和js数据对象上建立联系,自动展现。在上面的代码标签中data-bind
并不是Html中的原生标记,它在Html5中得到浏览器的支持,是KO框架用来进行声明式绑定的工具,所以在KO中进行声明式绑定,都通过data-bind属性进行。有了视图模型,有了相应视图,最后要进行两者的连接了,所以下面这行代码必不可少:
ko.applyBindings(myViewModel); //将myViewModel对象与UI中所有进行了声明式绑定的元素进行连接,注意:是所有。
完整测试代码:
The name is <span data-bind="text: personName"></span> @section scripts { <script src="~/Scripts/knockout-3.4.0.js"></script> <script type="text/javascript"> $(function () { var myViewModel = { personName: 'Bob', personAge: 123 }; ko.applyBindings(myViewModel); }); </script> }
关于ko.appyBindings(myViewModel)中参数的作用说明一下:第一个参数说明在整个UI中你希望使用哪个视图模型对象与视图中的声明绑定进行连接。你也可以传递第二个参数来决定这个视图模型与UI中的哪个特定的声明式绑定(data-bind)进行连接,而不是与所有的进行连接。举个例子, ko.applyBindings(myViewModel, document.getElementById('someElementId'))。
这就限制了这个视图模型对象只能与ID为someElementId
的Html元素对象以及它的后代元素对象进行连接,这样的话当你想要定义多个视图模型对象并且与页面中不同的元素进行绑定的时候就会特别有用。到目前为止真的是相当简单吧。
Observables
好了,你已经看到了如何创建一个基本的视图模型以及如何通过绑定进行它的属性数据的展现。但是使用KO一个核心的好处是当视图模型内容改变的时候它还会自动更新你的UI,反之亦然。这有时会大大简化你的代码(我们稍后展示这个效果)。那么KO如何知道你的视图模型什么时候发生了改变进而更新你的UI呢?答案是:你需要将你的数图模型中的属性声明为observable类型对象。 observables类型对象非常特殊,当视图模型发生改变的时候,他们可以向订阅者发出通知,并自动建立与订阅者的关系,订阅者也就是具有声明式绑定的元素。举个例子, 重写一下上面的代码如下:
$(function () { var myViewModel = { personName: ko.observable('Bob'), personAge: ko.observable(123) }; ko.applyBindings(myViewModel); });
现在,你完全不需要视图 data-bind
声明部分保持不变. 与之前代码不同的是,现在ko可以自动监测变化了, 一旦视图模型有数据发生变化,它就会自动更新视图。
Observables属性的读取与写入
要读取observable的当前值,只需要像调用方法一样以无参数的方式调用它。以上面的代码为例, myViewModel.personName()
将会返回 'Bob'
,而myViewModel.personAge()
将返回123。
要向observable属性中写入一个值的话也跟上面一样进行调用,只不过传入一个你想要写入的新值就可以了。举个例子:调用myViewModel.personName('Mary')
将会为personName赋一个新的名字。另外KO还提供了一个非常方便的代码链写法。 像这样:myViewModel.personName('Mary').personAge(50)
会修改personName为 'Mary'
,personAge修改为 50。
observables的核心作用就是"观察" ,也就是说, 被声明为observable的属性将来是会被双向通知的,它通知其它UI元素它已经被修改了,并且观察相关UI元素内容,并将变化值更新到ViewModel对象上。KO框架中的很多内置绑定就是用来干这事儿的。所以,当你在UI元素上(例如span标签)写上data-bind="text: personName"的时候
text
绑定类型将把这个span元素进行注册并做好被通知的准备只要视图对象上的personName发生了该表,span就会被通知修改内部的文本内容
(假设personName是一个observable值,另外除了text绑定还有许多其它类型绑定,我们后面提到)。
当你通过调用myViewModel.personName('Mary')来修改personName的值的时候
, text绑定将会自动更新相关DOM元素的text内容
。
observables的显示订阅处理
通常你无需干预订阅的过程,所以初学者可以暂时跳过这一小节。
当observable类型数据发生改变后如果你希望在这个过程中做一些处理,你可以调用observable属性上的subscribe方法来将自己的处理代码注册进来。举个例子:
myViewModel.personName.subscribe(function(newValue) { alert("The person's new name is " + newValue); });
上面这段代码执行后,如果修改了personName的值后,那么将会弹出一个警告框,并且通过newValue参数可以获取当前正在更新的值。这个过程叫订阅注册。
subscribe方法接受三个传入参数
: callback
是一个function当通知到来时会自动执行, target
(可选) 定义了在callback方法中 this代表了哪个对象(默认的话this就是当前视图模型对象)
, event
(可选; 默认值是"change"
) 事件名称,是指当什么类型的事件发生的时候会有通知到来,默认情况下就是当值发生改变的时候。(其它类型我们后面谈到)。
当然了,一旦你注册了一个自己的订阅,你也可以根据需要在未来的某个时候取消这个订阅,你需要先定义一个变量来接收subscribe当前的返回值,然后调用dispose方法。代码如下
var subscription = myViewModel.personName.subscribe(function (newValue) { /* do stuff */ }); // ...then later... subscription.dispose();
如果你希望在observable类型值在发生改变,但被赋值之前做一些处理的话,你也可以在beforeChange
事件上注册自己的处理,代码如下:
myViewModel.personName.subscribe(function(oldValue) { alert("The person's previous name is " + oldValue); }, null, "beforeChange");
注意: Knockout 是否触发上面的订阅还有一个默认条件就是新的值必须与老的值不相同,如果赋值时是相同值的话那么将不会触发这两个订阅。如果需要更改这种默认动作可以使用订阅器上的extend方法来修改。代码如下:
myViewModel.personName.extend({ notify: 'always' });
最后,如果你的observable属性在更新时的动作比较耗时或者会更新的很频繁,你可以通过限制通知的时间间隔,毕竟订阅通知会有性能影响。做法如下:
myViewModel.personName.extend({ rateLimit: 50 });
这样的话就算是频繁更新属性值,每次通知的事件间隔也会控制在50毫秒。测试代码如下:
<button type="button" id="btnStart">点击测试</button> <span id="clickcontent"></span> @section scripts { <script src="~/Scripts/knockout-3.4.0.js"></script> <script type="text/javascript"> $(function () { var myViewModel = { //personName: ko.observable("ZhouBo") personName: ko.observable("ZhouBo") }; myViewModel.personName.extend({ notify: 'always' }); myViewModel.personName.extend({ rateLimit: 5000 }); ko.applyBindings(myViewModel); var index = 0; $('#btnStart').click(function () { $('#clickcontent').text(++index); myViewModel.personName(++index); }); });
说明:如果我快速点击按钮 btnStart,则5秒钟之后第一个span的内容才会发生变化,也就是5庙后才发送了一次通知。