Knockout简介

1. MVVM 的概念

1.1 MVVM 的概念

MVC 模型示意图
MVC 模型示意图

MVVM 模型示意图
MVVM 模型示意图

1.2 双向绑定的概念

页面中每次状态的变化,一般都伴随着多次 DOM 操作,每次 DOM 操作一般都需要先找到该输入框元素,然后修改其相应属性,即便后来有了jQuery这种 DOM 操作神器,仍然非常繁琐。随着前端逻辑的日益复杂,前端越来越难以维护。此时双向绑定应运而生了。

双向绑定,简单地说,就是模型(数据)和 DOM 自动保持同步,模型变化了,会自动更新 DOM,用户操作了 DOM,会自动更新更新模型,并且触发相应事件。这样一来,我们只需要更新模型,以及监听模型变化就可以了,不再像以前一边更新模型,一边进行 DOM 操作了。

2. Knockout 的简介

Knockout是一个可以轻松实现双向绑定的库。它有以下特性:

2.1 模型和 DOM 双向绑定

这点不需要多作说明,这就是 Knockout 最大的意义所在。

2.2. 声明式绑定

使用简明易读的自定义属性data-bind将模型字段关联到 DOM 元素上。比如以下代码就将输入框和模型中的 name 关联起来。

1
<input type="text" data-bind="value:name" />

2.3 依赖跟踪

对于通过组合或转换而来的数据,保持其依赖链。请看如下例子。

1
2
3
4
5
6
7
8
9
function ViewModel() {
this.firstName = ko.observable('');
this.lastName = ko.observable('');
//声明 fullName 是由 firstName 和 lastName计算出来的
//当 firstName 和 lastName 其一发生变化时,fullName都会自动重新计算
this.fullName = ko.computed(function() {
return this.firstName() + ' ' + this.lastName();
}, this);
}

2.4 模板

也不用多解释,与模型关联的DOM就是一个模板。

2.5 其他一些特点

轻量(库,不是框架,侵入性低,很容和其他框架和库一起使用),全浏览器支持(包括IE6),没有依赖,免费(这是必须的)。

3. 简单入门

3.1 Bindings

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
-dom
|-visible---------------elem.style.display
|-text------------------elem.innerText
|-html------------------elem.innerHTML
|-css-------------------elem.className
|-style-----------------elem.style
|-attr------------------elem.setAttribute
-flow
|-foreach
|-if/ifnot
|-with
|-component-------------自定义组件
-form
|-click
|-submit
|-event-----------------用法:event:{keydown:onKeyDown}
|-enable/disable--------elem.disabled
|-value-----------------elem.value
|-textInput-------------elem.value(输入框值变化,model立即更新)
|-hasFocus--------------elem.focus()
|-checked---------------elem.checked
|-options---------------select的选项
|-selectedOptions-------select的选中选项
|-uniqueName------------如果:input 没有 name,则生成唯一的name。

3.2 官网链接

官网示例链接
系统自带绑定链接

4. 高级进阶

4.1 创建自定义绑定

使用方法:

1
<textarea data-bind="textInput:query"></textarea> <button data-bind="preview:query"></button>

创建方法:

1
2
3
4
5
6
7
8
9
10
11
ko.bindingHandlers.preview = {
init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
var query = ko.utils.unwrapObservable(valueAccessor());
//做一些初始化工作,比如事件绑定。
},
update: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
//这一行如果不加的话,query发生变化时,该方法不会被调用。
var query = ko.utils.unwrapObservable(valueAccessor());
//当query发生变化时,要做什么处理。
},
};

详细参考:knockout-preview.js

4.2 创建自定义组件

使用方法:

1
2
<!-- ko component:{name:"ko-dropmenu",params:{name:"agl-search-order",items:sorts}}
--><!-- /ko -->

创建方法:

1
2
3
4
5
ko.components.register('ko-dropmenu', {
viewModel: function(params){
this.name = ko.observable(params.name);
},
template: '<div data-bind="attr:{name:name}"></div>'

详细参考:knockout-dropmenu.js

4.3 computed 和 pureComputed

computedpureComputed均表示该字段是由其他一个或多个字段转化而来,其区别在于pureComputed会做一些优化,比如当前字段并没有显示在页面上时,该字段并不会被计算,当该字段显示时,才开始计算。

5. 最佳实践

5.1 label>radio/checkbox

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<div data-bind="visible:status==='show'">
<span data=bind="text:name"></span>
<label>
<input type="radio" value="edit" data-bind="checked:status">
<span>编辑</span>
</label>
</div>
<div data-bind="visible:status==='edit'">
<input type="text" data-bind="value:name">
<label>
<input type="radio" value="show" data-bind="checked:status">
<span>保存</span>
</label>
</div>

5.2 模型数据合理分块

当模型数据变化了以后,跟这些数据相关的 DOM 都会重新渲染,所以模型数据需要尽可能做到动静分离。
以收藏夹页面专利列表的详情模式为例,选中一个收藏夹,首先获取该收藏夹第一页的 50 条专利并渲染,然后再获取这 50 条专利的缩略图和 PDF 地址并渲染,用户可以选择全部选中和全部反选,也可以选择部分,然后标记为已读或未读。
如果将缩略图和 PDF 地址,是否选中,是否已读等字段作为专利数据的一部分,那么必然将造成多次专利列表的重新渲染。
一种可行的做法如下:

1
2
3
4
5
6
7
8
9
10
11
12
this.patents = ko.observableArray([]);

this.selected = ko.observableArray([]);
this.isAllSelected = ko.pureComputed({
read: function() {},
write: function() {},
owner: this,
});

this.readMap = ko.observable({});
this.thumbMap = ko.observable({});
this.pdfMap = ko.observable({});

5.3 全选

1
2
3
4
5
<input type="checkbox" data-bind="checked:isAllSelected" />

<ul data-bind="foreach:patents">
<li><input type="checkbox" data-bind="value:$data.PN,checked:$parent.selected" /></li>
</ul>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
this.selected = ko.observableArray([]);
this.isAllSelected = ko.pureComputed({
read: function() {
return this.selected().length === this.patents().length;
},
write: function(checked) {
var selected = null;
//如果全部选中
if (checked) {
selected = _.map(this.patents(), 'PN');
}
//如果全部反选
else {
selected = [];
}
this.selected(selected);
},
owner: this,
});

5.4 ViewModel 的重复利用

如果两个 ViewModel 都需要拥有一些相同的数据,我们当然可以通过pubsub事件模型来同步,但是这样可能导致 ViewModel 中导出都是同步逻辑,他们没有任何业务价值,并且影响后来维护者理解这段代码。有什么方法解决这个问题吗?答案是肯定的,请看一下代码。

1
2
3
4
5
6
7
8
9
var userViewModel = {
user: ko.observable({});
};
var model1 = {
user: userViewModel
}
var model2 = {
user: userViewModel
}

在以上这段代码中,model1model2中拥有同一份user数据,自然是自动同步的。

6. 踩过的坑

6.1 声明绑定时忘记带括号

如果直接绑定字段,才可以省略括号,如果是表达式,则必须带括号,这点初学者很容易犯错,建议所有的绑定都带括号。

1
this.index = ko.observable(0);
1
2
3
4
5
6
7
8
<!-- 正确,绑定单个字段可以不带括号 -->
<span data-bind="text:index"></span>
<!-- 正确,绑定单个字段带括号也是对的 -->
<span data-bind="text:index()"></span>
<!-- 错误,绑定表达式时必须带括号 -->
<span data-bind="text:'NO.:' + index"></span>
<!-- 正确,绑定表达式时必须带括号 -->
<span data-bind="text:'NO.:' + index()"></span>

6.2 Class 属性的绑定

Class 绑定方法有两种:
css

1
<div data-bind="css:{disabled:status()==='disabled'}"></div>

class

1
<div data-bind="attr:{'class':status}"></div>

假设 ViewModel 中 status 的值为 ‘disabled’,则以上两种绑定都会给元素添加一个disabled类名。
使用后者时需要注意class一定要加上引号,否则在 IE8 中报错。

6.3 jQuery.fn.data 的缓存

jQuery中,jQuery.fn.data方法是有缓存的,如果要获取正确的结果,必须通过jQuery.fn.data进行设置data属性,而不使用原生方法HTMLElement.prototype.getAttribute

1
2
<!-- 分页器中的页面跳转链接 -->
<span data-bind="click:goToPage,attr:{'data-page':page()-1}">上一页</span>
1
2
3
4
5
6
7
this.page = ko.observable(0);
this.goToPage = function(model, e) {
//错误,因为缓存问题,导致第二次获取页面时出错。
var page = $(e.target).data('page');
//正确
var page = e.target.getAttribute('data-page');
};

6.4 坑爹的性能问题

是否停止运行此脚本图片

Knockout 的模板是基于 DOM 的,遇到循环就会通过原生的 clone 方法复制出若干个 DOM 片段,这个方法性能很差,当复制的 DOM 节点数达到一定程度,就会变得很慢,尤其在 IE8 中,收藏夹中渲染专利列表时就遇到了这种问题,在 IE8 甚至出现了“是否停止运行此脚本对话框”。
由于该问题是在测试阶段发现的,没有时间进行大的修改。但是 IE8 中的对话框又是不能接受的,所以改为首次渲染前 25 条(如果有的话)专利,异步等 100 毫秒之后再渲染后 25 条(如果有的话)专利。这样,所有专利渲染出来的时间虽然延长了一点,但是保证了不会弹出让人费解的对话框,并且对用户体验基本没有更坏的影响。

7. 参考链接

  1. Knockout 官网
  2. MVC,MVP 和 MVVM 的图示