1. MVVM 的概念
1.1 MVVM 的概念
MVC 模型示意图
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 | function ViewModel() { |
2.4 模板
也不用多解释,与模型关联的DOM就是一个模板。
2.5 其他一些特点
轻量(库,不是框架,侵入性低,很容和其他框架和库一起使用),全浏览器支持(包括IE6),没有依赖,免费(这是必须的)。
3. 简单入门
3.1 Bindings
1 | -dom |
3.2 官网链接
4. 高级进阶
4.1 创建自定义绑定
使用方法:
1 | <textarea data-bind="textInput:query"></textarea> <button data-bind="preview:query"></button> |
创建方法:
1 | ko.bindingHandlers.preview = { |
详细参考:knockout-preview.js
4.2 创建自定义组件
使用方法:
1 | <!-- ko component:{name:"ko-dropmenu",params:{name:"agl-search-order",items:sorts}} |
创建方法:
1 | ko.components.register('ko-dropmenu', { |
详细参考:knockout-dropmenu.js
4.3 computed 和 pureComputed
computed
和pureComputed
均表示该字段是由其他一个或多个字段转化而来,其区别在于pureComputed
会做一些优化,比如当前字段并没有显示在页面上时,该字段并不会被计算,当该字段显示时,才开始计算。
5. 最佳实践
5.1 label>radio/checkbox
1 | <div data-bind="visible:status==='show'"> |
5.2 模型数据合理分块
当模型数据变化了以后,跟这些数据相关的 DOM 都会重新渲染,所以模型数据需要尽可能做到动静分离。
以收藏夹页面专利列表的详情模式为例,选中一个收藏夹,首先获取该收藏夹第一页的 50 条专利并渲染,然后再获取这 50 条专利的缩略图和 PDF 地址并渲染,用户可以选择全部选中和全部反选,也可以选择部分,然后标记为已读或未读。
如果将缩略图和 PDF 地址,是否选中,是否已读等字段作为专利数据的一部分,那么必然将造成多次专利列表的重新渲染。
一种可行的做法如下:
1 | this.patents = ko.observableArray([]); |
5.3 全选
1 | <input type="checkbox" data-bind="checked:isAllSelected" /> |
1 | this.selected = ko.observableArray([]); |
5.4 ViewModel 的重复利用
如果两个 ViewModel 都需要拥有一些相同的数据,我们当然可以通过pubsub
事件模型来同步,但是这样可能导致 ViewModel 中导出都是同步逻辑,他们没有任何业务价值,并且影响后来维护者理解这段代码。有什么方法解决这个问题吗?答案是肯定的,请看一下代码。
1 | var userViewModel = { |
在以上这段代码中,model1
和model2
中拥有同一份user
数据,自然是自动同步的。
6. 踩过的坑
6.1 声明绑定时忘记带括号
如果直接绑定字段,才可以省略括号,如果是表达式,则必须带括号,这点初学者很容易犯错,建议所有的绑定都带括号。
1 | this.index = ko.observable(0); |
1 | <!-- 正确,绑定单个字段可以不带括号 --> |
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 | <!-- 分页器中的页面跳转链接 --> |
1 | this.page = ko.observable(0); |
6.4 坑爹的性能问题
Knockout 的模板是基于 DOM 的,遇到循环就会通过原生的 clone 方法复制出若干个 DOM 片段,这个方法性能很差,当复制的 DOM 节点数达到一定程度,就会变得很慢,尤其在 IE8 中,收藏夹中渲染专利列表时就遇到了这种问题,在 IE8 甚至出现了“是否停止运行此脚本对话框”。
由于该问题是在测试阶段发现的,没有时间进行大的修改。但是 IE8 中的对话框又是不能接受的,所以改为首次渲染前 25 条(如果有的话)专利,异步等 100 毫秒之后再渲染后 25 条(如果有的话)专利。这样,所有专利渲染出来的时间虽然延长了一点,但是保证了不会弹出让人费解的对话框,并且对用户体验基本没有更坏的影响。