2. 控制流
1. foreach绑定
目的
foreach
绑定会遍历一个数组,为每个数组项生成重复的元素标记结构并做关联。这在渲染列表或表格的时候特别有用。
假设你的数组是一个监控数组
,之后无论你进行添加,移除还是重新排序,对应的UI结构也会发生相应变化 -- 插入或移除标记结构,或者重排已存在的DOM元素。这不会影响其他的DOM元素,这远比数组发生改变后重新渲染生成foreach输出结构快多了。
当然,你可以嵌套任意数量的foreach
绑定,或者其他的控制流绑定,比如if
绑定和with
绑定。
例子1:遍历一个数组
这个例子用foreach
生成一个只读表格,每个数组项对应一行。
<table>
<thead>
<tr><th>First name</th><th>Last name</th></tr>
</thead>
<tbody data-bind="foreach: people">
<tr>
<td data-bind="text: firstName"></td>
<td data-bind="text: lastName"></td>
</tr>
</tbody>
</table>
<script type="text/javascript">
ko.applyBindings({
people: [
{ firstName: 'Bert', lastName: 'Bertington' },
{ firstName: 'Charles', lastName: 'Charlesforth' },
{ firstName: 'Denise', lastName: 'Dentiste' }
]
});
</script>
例子2:add/remove实例
view:
<h4>People</h4>
<ul data-bind="foreach: people">
<li>
Name at position <span data-bind="text: $index"> </span>:
<span data-bind="text: name"> </span>
<a href="#" data-bind="click: $parent.removePerson">Remove</a>
</li>
</ul>
<button data-bind="click: addPerson">Add</button>
viewmodel:
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);
}
}
ko.applyBindings(new AppViewModel());
参数
-
主参数
传入你想要遍历的数组,foreach绑定会为每个实体生成一个对应的标记段。另外,可以传入一个javascript对象字面量的如叫
data
的属性,也就是你想遍历的数组,这个对象字面量可能有其他属性,比如afterAdd
或者include Destroyed
,在下面看这些扩展项的详细内容并看在例子中如何使用的。
如果你的数组是一个监控数组,foreach
绑定会对数组的内容发生的任何改变都会做出响应,即通过添加或删除相应的DOM标记块。 -
额外参数
无
注意1:通过$data引用数组中的每个数据实体
如上面的例子所示,foreach
绑定可以遍历数组并引用实体的属性。比如,例子1引用了每个数组实体的firstName
和 lastName
属性。
但是如果你想引用实体本身怎么办(而不是实体的属性)?在这种情况下,你可以使用特殊的上下文属性$data
。在foreach
标记块里面,它的意思是当前项的意思。例子:
<ul data-bind="foreach: months">
<li>
The current item is: <b data-bind="text: $data"></b>
</li>
</ul>
<script type="text/javascript">
ko.applyBindings({
months: [ 'Jan', 'Feb', 'Mar', 'etc' ]
});
</script>
只要你想,你可以使用$data
作为引用每个属性的前缀,比如,例子1可以改为如下形式:
<td data-bind="text: $data.firstName"></td>
但是你没有必要这样做,因为默认情况下,fristName
会在$data
上下文下计算。
注意2:使用(index,)parent和其他上下文属性
如上面例子2所示, 你可以使用$index
来获取当前项的索引值(从0开始),$index
是一个监控对象,它会在数组项发生改变的时候自动更新(如:从数组里面添加或移除数组项)。
同样的,你可以使用$parent
来引用当前foreach
绑定的外层数据,例子如下:
<h1 data-bind="text: blogPostTitle"></h1>
<ul data-bind="foreach: likes">
<li>
<b data-bind="text: name"></b> likes the blog post <b data-bind="text: $parent.blogPostTitle"></b>
</li>
</ul>
想要了解$index
和$parent
的更多信息和其他的上下文属性,请看文档5.2绑定上下文
注意3:使用 as 给 foreach项取别名
如例子1所示,你可以通过$data
上下文变量引用每个数组实体。在某些情况下,给当前对象一个更具有可读性的名字很有用,可以使用 as
关键字达到目的:
<ul data-bind="foreach: { data: people, as: 'person' }"></ul>
现在在foreach
代码块内,绑定都可以使用person
来表示people
数组中的当前项,这在你存在嵌套foreach
绑定,,内层的foreach
循环想引用更高一层的foreach
循环的项的情况下特别有用,例子如下:
<ul data-bind="foreach: { data: categories, as: 'category' }">
<li>
<ul data-bind="foreach: { data: items, as: 'item' }">
<li>
<span data-bind="text: category.name"></span>:
<span data-bind="text: item"></span>
</li>
</ul>
</li>
</ul>
<script>
var viewModel = {
categories: ko.observableArray([
{ name: 'Fruit', items: [ 'Apple', 'Orange', 'Banana' ] },
{ name: 'Vegetables', items: [ 'Celery', 'Corn', 'Spinach' ] }
])
};
ko.applyBindings(viewModel);
</script>
提示:传递给 as
的字符串字面量(如, as: 'category'
, 而不是 as: category
),因为您是创建一个名字为传过来的字符串字面量的新变量,而不是读取一个已存在的变量的值。
注意 4: 不使用包含容器使用foreach
在某些情况下,你可能想要重复渲染一部分HTML标签,但是你没有任何元素标签用来绑定foreach
绑定。比如,你可能会想实现如下情形:
<ul>
<li class="header">Header item</li>
<!-- 下面内容由数组动态渲染生成 -->
<li>Item A</li>
<li>Item B</li>
<li>Item C</li>
</ul>
在这个例子里,<ul>
标签里没有合适的地方放置一个一般的foreach
绑定(因为你不想重复渲染header item项),你也不能在<ul>
里面再嵌套一个容器标签(因为在<ul>
标签里面只允许插入<li>
标签)。
为了处理这种情况,你可以使用无容器的控制流语法,这个基于注释标签来实现,例子如下:
<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>
The <!-- ko -->
and <!-- /ko -->
注释作为开始/结束标记, 在容器里面定义一个“虚拟元素”。 Knockout 可以理解虚拟元素这种语法,然后就像绑定在一个真正的容器元素上一样。
注意 5: 怎样检测到数组改发生了变化并进行相应处理
当你修改你的模型数组的内容(通过增加、移动、删除里面的实体),foreach绑定会使用一个高效的差异算法找出什么发生了改变,这样就能更新对应的DOM元素节点。这意味着它可以处理同时改变的任意组合。
- 当你添加数组实体,foreach会把你设置的模板复制一个新的副本然后插入已存在的DOM节点里呈现出来。
- 当你删除数组实体,foreach删除对应的DOM元素节点。
- 当你对数组实体进行重排(对象实体没有增删),foreach通常只会移动对应的DOM元素到新的位置
需要注意的是重排并不是十分可靠:为了确保算法能高效执行,它被优化为可以检测少量的数组实体的 简单 移动。如果算法检测到大量的同时排序组合,而且与插入和删除无关,这时候为了执行速度,它会通过 “删除”和“添加”来代替简单的 “移动”,在这种情况下,对应的DOM元素节点会删除,然后重新添加。大多数开发者不会遇到这种极端情况,甚至你遇到了,最终用户体验仍然是一致的。
注意 6: 默认通过隐藏实体来表示销毁实体
有时你想要删除一个数组实体,但是实际上并没有真正的删除。这就是所谓的非破坏性删除。想明白具体是怎么实现的,请看pobservableArray的destroy函数。
默认情况下,foreach绑定会跳过(比如,隐藏)任何标记为destroyed
的数组实体。如果你想要显示销毁的实体,你可以使用includeDestroyed
选项,如下所示:
<div data-bind='foreach: { data: myArray, includeDestroyed: true }'>
...
</div>
注意 7: 生成DOM元素过程动画化或生成后进行处理
如果你想在生成DOM元素后进行一些自定义逻辑操作,你可以使用afterRender/afterAdd/beforeRemove/beforeMove/afterMove
这些回调函数。在下面有这些函数的说明。
注意:这些回调函数仅用于通过列表中的变化触发相关的动画。如果你想在新的DOM节点被添加的时候附加一些其他行为(比如,进行事件处理,调用第三方插件)。而你把你想实现的新行为作为自定义绑定反而会更简单,因为之后你可以在任何地方使用你实现的新行为,独立于foreach
绑定。
这是一个应用afterAdd
的简单例子,在添加新项的时候使用经典的“黄色淡出”效果。这个需要jQuery 颜色插件来保证背景颜色动画可用。
<ul data-bind="foreach: { data: myItems, afterAdd: yellowFadeIn }">
<li data-bind="text: $data"></li>
</ul>
<button data-bind="click: addItem">Add</button>
<script type="text/javascript">
ko.applyBindings({
myItems: ko.observableArray([ 'A', 'B', 'C' ]),
yellowFadeIn: function(element, index, data) {
$(element).filter("li")
.animate({ backgroundColor: 'yellow' }, 200)
.animate({ backgroundColor: 'white' }, 800);
},
addItem: function() { this.myItems.push('New item'); }
});
</script>
API详情:
afterRender
-在foreach第一次初始化的时候关联数组中每个实体和关联数组添加新实体两种情况下,通过模板复制出对应的DOM节点并插入文档后触发,删除节点不会触发,knockout会在回调函数提供如下参数:
- 要插入的dom元素的dom节点数组(也就是foreach绑定内的所有dom节点,如果存在换行,换行符会识别为#text节点)
- 正要进行绑定的数据项
afterAdd
- 和afterRender
类似,但是仅仅在新实体添加到数组的时候触发(在foreach第一次遍历数组的初始内容的时候不会触发)。afterAdd
通常用于添加效果。如jQuery的$(domNode).fadeIn()
。这样添加新项的时候会有一个动画效果。knockout会在回调函数提供如下参数:
- 将要插入document的DOM节点
- 将要插入数组的节点的索引值
- 要添加的数组元素
beforeRemove
- 在数组项要被移除的时候触发。但在对应的DOM节点移除之前。如果你指定了一个beforeRemove
回调函数,你需要手动去删除Dom节点。常见场景比如使用jQuery’的$(domNode).fadeOut()
来给删除节点添加动画— 在这种情况下, Knockout 不能知道DOM节点实际上要多久才会删除(谁知道你的动画会占多少时间),所以应该由您删除DOM节点。knockout会在回调函数提供如下参数:
- 您要移除的DOM节点
- 要移除的元素在数组中的索引值
- 要移除的数组元素
beforeMove
- 在数组项改变了在数组中的位置之前触发,在DOM节点移动之前。需要注意的是beforeMove
会使所有的数组元素的索引发生改变,所以如果你想在数组的头部添加一个新项, 然后回调函数(如果有定义)会被剩余的元素触发,因为所有的元素的索引值都加一,你可以使用beforeMove
存储移动之前的数组元素的原始位置,这样你可以利用存储的数据在afterMove
生成移动特效.。knockout会在回调函数提供如下参数:
- 可能被移动的DOM节点
- 要移动的数组实体在数组中的位置
- 要移动的数组实体
afterMove
- 在数组项改变了在数组中的位置之后触发。在foreach更新了对应等等DOM节点之后,需要注意的是afterMove
会被所有索引发生改变的数组元素的触发,所以如果你在数组的头部插入一个新项,回调函数(如果有定义)会被剩余的元素触发,因为所有的元素的索引值都加一。。knockout会在回调函数提供如下参数:
- 可能被移动的DOM节点
- 要移动的数组实体在数组中的位置
- 要移动的数组实体
要看应用afterAdd
和beforeRemove
的例子,请跳转到animated transitions查看。
最好通过调试一个例子来查看:
html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
<link rel="stylesheet" type="text/css" href="index.css"/>
<script src="../../js/lib/jquery-1.7.2/jquery-1.7.2.js" type="text/javascript" charset="utf-8"></script>
<script src="../../js/lib/knockout/knockout-3.3.0.js" type="text/javascript" charset="utf-8"></script>
<script src="index.js" type="text/javascript" charset="utf-8"></script>
</head>
<body>
<input type="button" value="add" data-bind="click:add" />
<input type="button" value="delete" data-bind="click:del" />
<input type="button" value="move first to last" data-bind="click:move" />
<ul class="list" data-bind = "foreach:{data:list,afterRender:afterRender,afterAdd:afterAdd,beforeRemove:beforeRemove,beforeMove:beforeMove,afterMove:afterMove}">
<li data-bind = "text:text"></li>
</ul>
<ul data-bind="foreach:loglist" style=" 300px;height:600px;overflow:auto;border: 1px solid red;position: absolute;top: 0;right: 0;display: block;">
<li data-bind = "text:text"></li>
</ul>
</body>
</html>
javascript:
function main() {
var vm = {
list: ko.observableArray([{
text: "aaa"
}, {
text: "bbb"
}, {
text: "ccc"
}]),
loglist: ko.observableArray(),
add: function() {
this.list.push({
text: "time:"+Math.random().toFixed(5)
});
},
move:function(){
var item = this.list()[0];
this.list.remove(item);
$(".list >li:first").remove();
this.list.push(item);
},
del:function(){
var item=this.list()[0];
this.list.remove(item);
},
afterRender: function(eles,obj) {
// debugger;
// vm.loglist.push({text:"afterRender:"+obj.text});
},
afterAdd: function(ele,index,obj) {
// debugger;
// vm.loglist.push({text:"afterAdd:index:"+index+"text:"+obj.text});
},
beforeRemove: function(ele,index,obj) {
// debugger;
// vm.loglist.push({text:"beforeRemove:index:"+index+"text:"+obj.text});
// ele.parentNode.removeChild(ele);
},
beforeMove: function(ele,index,obj) {
// debugger;
// vm.loglist.push({text:"beforeMove:index:"+index+"text:"+obj.text});
},
afterMove: function(ele,index,obj) {
// debugger;
// vm.loglist.push({text:"afterMove:index:"+index+"text:"+obj.text});
},
};
ko.applyBindings(vm);
}
window.onload = function() {
main();
}
依赖
只依赖核心库