(三)Knockout 控制流程
foreach
示例1:迭代数组
foreach binding主要作用于lists或是tables内数据单元的动态绑定。下面是一个简单的例子:
<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">
var myViewModel = {
people: [
{ firstName: "Chiaki", lastName: "Izumi" },
{ firstName: "Kazusa", lastName: "Touma" },
{ firstName: "Haruki", lastName: "Murakami" }
]
};
ko.applyBindings(myViewModel);
</script>
示例2:添加 / 删除的实例
在上述示例中,我们简单的在ko.applybindings中添加了一个数组并将其绑定在一个tbody元素中,我们也可以自定义一个view model来实现这一效果,下面是一个更为复杂一些的例子:
<h4>People</h4>
<ul data-bind="foreach: people">
<li>
Person 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>
<script type="text/javascript">
function MyViewModel() {
var self = this;
self.people = ko.observableArray([
{ name: "Chiaki" },
{ name: "Yuki" },
{ name: "Kazusa" }
]);
self.addPerson = function() {
self.people.push({ name: "New name at " + new Date() });
};
self.removePerson = function() {
self.people.remove(this);
};
}
ko.applyBindings(new MyViewModel());
</script>
参数
- 主要参数
传递希望迭代的数组。绑定将为每个条目输出一段标记。
或者,传递一个JavaScript对象文本和一个名为data
的属性,该属性是您希望迭代的数组。对象文字还可能具有其他属性,如afterAdd
或includeDestroyed
,有关这些额外选项的详细信息及其使用示例,请参见下面。
如果您提供的数组是可见的,那么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的部分重写如下
<!-- $data 引用对象每个值-->
<td data-bind="text: $data.firstName"></td>
但您不必这样做,因为firstName在默认情况下将在$data
上下文中进行计算。如果数组中的项是被监控的,$data
将引用每个监控的值。要引用可观察对象本身,推荐使用$rawData
。
<!--$rawDat 引用对象本身-->
<td data-bind="text: $rawData.firstName"></td>
注释2:使用$index、$parent和其他上下文属性
从上面的示例2可以看出,可以使用$index
引用当前数组项的从零开始的索引。$index
是一个可观察的对象,当项目的索引发生变化时(例如,如果项目被添加到数组中或从数组中删除),$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>
<script type="text/javascript">
function AppViewModel() {
var self = this;
self.blogPostTitle =ko.observable('你好');
self.likes = ko.observableArray([
{ name: 'Bert' },
{ name: 'Charles' },
{ name: 'Denise' }
]);
}
ko.applyBindings(new AppViewModel());
</script>
注释3:使用 “as” 的别名为 “foreach” 项目
在注释1中提到,我们能够通过$data来调用foreach绑定的数组本身,但是当我们使用嵌套的foreach,内层foreach如何能够调用外层foreach绑定的数组呢?这时我们可以借由as给外层foreach所绑定的数组定义一个另外的名称,进而在内层foreach中利用此名称来调用外层foreach所绑定的数组。接下来是一个简单的例子:
<ul data-bind="foreach: { data: person, as: 'person' }">
<li>
<ul data-bind="foreach: { data: friends, as: 'friends' }">
<li>
<span data-bind="text: person.name"></span>:
<span data-bind="text: friends"></span>
</li>
</ul>
</li>
</ul>
<script>
var viewModel = {
person: ko.observableArray([
{ name: "Chiaki", friends: ["Charlie", "Kazusa"] },
{ name: "Kazusa", friends: ["Chiaki", "Charlie"] },
{ name: "Charlie", friends: ["Chiaki", "Kazusa"] }
])
};
ko.applyBindings(viewModel);
</script>
这个例子中的外层foreach绑定的是person数组,person数组中有一个属性name和另外一个数组firends,在内层foreach中绑定的是数组firends。当我们在内层foreach要调用外层的person数组内的属性时,借由as,使用了person.name来调用。而这里也有一个有趣的情况,就是当一个数组里面只有单纯的元素,比如说friends数组,它的元素并不是object,也就不存在这identifier的问题,这时我们要调用它的元素时,只需要调用数组本身即可,这也就是为什么在之前的示例中如果我们调用绑定的数组本身会返回[object, object]。
这表明,一般情况下,遍历数组中的元素只需要调用数组名(as指定)或是调用$data即可,而碰到那些内部元素是object的时候,我们要调用object内的属性则需要调用相应属性的名称。
另外需要注意的一点就是,as后所跟着的必须是一个字符串(as: "person"而不是as: person)。
注释4:不使用foreach当容器
有些情况下,我们使用foreach的场合会比较复杂,比如说如下的例子:
<ul>
<li>Header item</li>
<!-- The following are generated dynamically from an array -->
<li>Item A</li>
<li>Item B</li>
<li>Item C</li>
</ul>
这种情况下,我们并不知道改在哪个元素内添加foreach。如果是在ul添加的话,我们事先写好的header item便会被覆盖掉,而ul元素内又只能嵌套li元素,添加另一个容器来实现foreach也是不可行的。为了解决这一问题,我们需要使用无容器的控制流语法(containerless control flow syntax),与先前所提到的containerless text syntax类似。一个简单的例子如下:
<!-- 不使用foreach,使用容器 -->
<ul>
<li>Header item</li>
<!-- ko foreach: people -->
<li>name: <span data-bind="text: $data"></span></li>
<!-- /ko -->
</ul>
<!-- 使用foreach -->
<ul data-bind="foreach: people">
<li>Header item</li>
<li>name: <span data-bind="text: $data"></span></li>
</ul>
<script>
var viewModel = {
people: ko.observableArray(["Kazusa", "Chiaki", "Yuki"])
};
ko.applyBindings(viewModel);
</script>
注释7:处理后所生成的 DOM 元素
当我们需要在生成的DOM元素上运行一些自定义的逻辑时,我们可以用到
- afterRender
- afterAdd
- beforeRemove
- beforeMove
- afterMove
等回调函数。
需要注意的是,这些回调函数仅仅适用于触发与列表的改变有关的动画,如果我们需要对新添加的DOM元素附加一些其他的行为,比如说事件的处理或是第三方的UI控制,将这些行为作为自定义的绑定(custom binding)会更为合适,因为这样设定的行为是与foreach互相独立的。
接下来是一个调用afterAdd的简单的例子,其中用到了jQuery Color plugin。
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.11.1/jquery.js"></script>
<script src="http://code.jquery.com/color/jquery.color-2.1.2.min.js"></script>
<script src="../js/knockout-3.5.0rc2.debug.js"></script>
<ul data-bind="foreach: { data: people, afterAdd: yellowFadeIn }">
<li data-bind="text: $data"></li>
</ul>
<button data-bind="click: addItem">Add</button>
<script>
var viewModel = {
people: ko.observableArray(["Kazusa", "Chiaki", "Yuki"]),
yellowFadeIn: function(element, index, data) {
$(element)
.filter("li")
.animate({ backgroundColor: "yellow" }, 200)
.animate({ backgroundColor: "white" }, 800);
},
addItem: function() {
this.people.push("New person");
}
};
ko.applyBindings(viewModel);
</script>
以下是对一些回调函数详尽的介绍:
afterRender
是在foreach模块初始化或是添加新的元素时触发的,其接受的参数为(为了能够保持愿意,这里用英文显示,下同):
An array of the inserted DOM elements
The data item against which they are being bound
afterAdd
与afterRender
类似,不过它只会在新元素添加时触发(foreach初始化的时候并不会触发),它所接受的参数为:
A DOM node being added to the document
The index of the added array element
The added array element
beforeRemove
函数会在数组中的某一项被删除时触发。需要注意的是,beforeRemove实际上替代了UI界面对remove所做出的回应,即在beforeRemove函数中如果不对DOM相应的元素进行remove操作,则在页面上的元素是不会被删除的,但是viewModel中的数组相应的元素却已经被删除了。beforeRemove函数接受以下参数:
A DOM node that you should remove
The index of the removed array element
The removed array element
beforeMove
函数会在数组中的元素索引发生变化时触发,beforeMove会应用于所有索引产生变化的元素,即假若我们在数组开头插入元素,则其后所有元素都将受到beforeMove回调函数的影响。一个比较常用的做法是通过beforeMove来保存原有元素的坐标以便我们能够在afterMove中控制元素移动的动画。beforeMove函数接受以下参数:
A DOM node that may be about to move
The index of the moved array element
The moved array element
afterMove
函数也会在数组中的元素索引发生变化时触发,afterMove会应用于所有索引产生变化的元素,即假若我们在数组开头插入元素,则其后所有元素都将受到afterMove回调函数的影响。afterMove函数接收以下参数:
A DOM node that may have moved
The index of the moved array element
The moved array element
对于回调函数中的before
和after
,我们应该有一个比较清醒的认识。before和after针对的都是UI界面中的元素变化,也就是页面产生变化之前和页面产生变化之后,与此同时,viewModel部分的数组已经发生了变化,正是viewModel部分的数组的变化才触发了before和after所对应的回调函数。
if和ifnot绑定
if binding与visible binding类似。不同之处在于,包含visible binding的元素会在DOM中一直保存,并且该元素相应的data-bind属性会一直保持,visible binding只是利用CSS来触发元素的可见性。另一方面,if binding是物理地增加或删除包含它的元素,并且元素内的data-bind只有在判断语句为真时才生效。
例子1
下面是一个简单的if binding的例子:
<label>
<input type="checkbox" data-bind="checked: displayMessage" />
Display message
</label>
<div data-bind="if: displayMessage">Here is a message. Astonishing.</div>
<script type="text/javascript">
ko.applyBindings({
displayMessage: ko.observable(false)
});
</script>
例子2
在下面的例子中,<div>
元素对于“Mercury”是空的,但是对于“Earth”是填充的。这是因为 Earth 有一个非空 capital
属性,而 Mercury 的 capital 属性为 null
。
<ul data-bind="foreach: planets">
<li>
Planet: <b data-bind="text: name"> </b>
<div data-bind="if: capital">
Capital: <b data-bind="text: capital.cityName"> </b>
</div>
</li>
</ul>
<script>
ko.applyBindings({
planets: [
{ name: 'Mercury', capital: null },
{ name: 'Earth', capital: { cityName: 'Barnsley' } }
]
});
</script>
重要的是要理解 if
绑定对于使代码正常工作非常重要。没有它,在评估时就会出现错误。“Mercury”上下文中的 capital.cityName
,其中 capital
为 null
。在JavaScript中,不允许计算空值或未定义值的子属性。
无容器
if binding也支持无容器的控制流语法,一个简单的示例如下:
<label>
<input type="checkbox" data-bind="checked: displayMessage" />
Display message
</label>
<ul>
<li>Item 1</li>
<!-- ko if: displayMessage -->
<li>Message</li>
<!-- /ko -->
</ul>
<div data-bind="if: displayMessage">Here is a message. Astonishing.</div>
<script type="text/javascript">
ko.applyBindings({
displayMessage: ko.observable(false)
});
</script>
ifnot
<div data-bind="ifnot: someProperty">...</div>
等价于
<div data-bind="if: !someProperty()">...</div>
with和using绑定
with
和 using
绑定创建一个新的绑定上下文,以便将后代元素绑定到指定对象的上下文中。(这些绑定之间的区别将在下面的参数中描述。)
当然,您可以任意嵌套使用with
绑定以using
及其他控制流绑定(如if
和foreach
)。
例子1:
下面是一个将绑定上下文切换到子对象的非常基本的示例。注意,在 data-bind
属性中,没有必要在经纬度前面加上coords
。,因为绑定上下文已切换到coords
。
这里也可以使用 with
。
<h1 data-bind="text: city"> </h1>
<p data-bind="using: coords">
Latitude: <span data-bind="text: latitude"> </span>,
Longitude: <span data-bind="text: longitude"> </span>
</p>
<script type="text/javascript">
ko.applyBindings({
city: "London",
coords: {
latitude: 51.5001524,
longitude: -0.1262362
}
});
</script>
例子2
<form data-bind="submit: getTweets">
Twitter account:
<input data-bind="value: twitterName" />
<button type="submit">Get tweets</button>
</form>
<div data-bind="with: resultData">
<h3>
Recent tweets fetched at <span data-bind="text: retrievalDate"> </span>
</h3>
<ol data-bind="foreach: topTweets">
<li data-bind="text: text"></li>
</ol>
<button data-bind="click: $parent.clearResults">Clear tweets</button>
</div>
<script type="text/javascript">
function AppViewModel() {
var self = this;
self.twitterName = ko.observable("@example");
self.resultData = ko.observable(); // No initial value
self.getTweets = function() {
var name = self.twitterName(),
simulatedResults = [
{ text: name + " What a nice day." },
{ text: name + " Building some cool apps." },
{ text: name + " Just saw a famous celebrity eating lard. Yum." }
];
self.resultData({
retrievalDate: new Date(),
topTweets: simulatedResults
});
};
self.clearResults = function() {
self.resultData(undefined);
};
}
ko.applyBindings(new AppViewModel());
</script>
从这里例子中,我们可以看出with binding在使用时的几个特点。
- 当with binding所绑定的binding context为null或是undefined时,包含with binding的元素的所有子元素都将从UI页面中移除。
- 如果我们需要从parent binding context中获取data或是function,我们可以使用特殊的context properties比如说
$parent
或root
,这部分可以参考The binding context。
倘若绑定的binding context是一个observable,包含with binding的元素会随着observable的变化而移除现有的子孙元素并添加一系列隶属于新的binding context的子孙元素。
类似的,with binding也提供无容器的控制流语法,这里省略例子,可以参考if binding等。
with和using区别
如果您提供的表达式包含任何监控的值,则每当这些值发生更改时,将重新计算该表达式。这些绑定在绑定值发生变化时的反应不同:
- 对于with绑定,将清除后代元素,并将标记的新副本添加到文档中,并在新值的上下文中绑定。
- 对于using绑定,后代元素将保留在文档中,并使用新的上下文值重新评估它们的绑定。
附加参数
- as
as选项允许为新上下文对象设置别名。虽然您可以使用$data
上下文变量引用对象,但是使用as选项为它提供一个更具描述性的名称可能会很有用
<div data-bind="with: currentPerson, as: 'person'"></div>
- noChildContext
as
选项的默认行为是为提供的对象设置一个名称,同时仍然将内容绑定到对象。但是您可能更愿意保持上下文不变,只设置对象的名称。后一种行为可能是将来版本的击倒的默认行为。若要打开特定绑定,请将noChildContext
选项设置为true
。当这个选项与as
一起使用时,对对象的所有访问都必须通过给定的名称,并且$data
将保持设置为外部viewmodel。
对于using绑定,虽然您可以使用此选项,但使用let绑定通常会更有效和描述性。 而不是using: currentPerson, as: 'person', noChildContext: true
,你可以使用let: { person: currentPerson }
。
注释1:无容器
与其他控制流绑定(如if和foreach)一样,可以使用with和using而不使用任何容器元素来承载它。如果您需要在不合法的地方使用这些绑定,仅仅为了保存绑定而引入新的容器元素,那么这是非常有用的。
<ul>
<li>Header element</li>
<!-- ko with: outboundFlight -->
...
<!-- /ko -->
<!-- ko with: inboundFlight -->
...
<!-- /ko -->
</ul>
注释2:为什么使用两个类似的绑定?
当不需要重新呈现后代元素时,他在knockoutjs 3.5中using
了绑定来替代with
。(参考with和using区别) 因为using
重新评估后代绑定而不是重新呈现,每个后代绑定将包含对使用上下文的附加依赖。
component 绑定
component 组件绑定将指定的组件注入元素,并可选地向其传递参数。
示例1:计算字数
<h4>First instance, without parameters</h4>
<div data-bind='component: "message-editor"'></div>
<h4>Second instance, passing parameters</h4>
<div data-bind='component: {
name: "message-editor",
params: { initialText: "Hello, world!" }
}'></div>
<script type="text/javascript">
ko.components.register('message-editor', {
viewModel: function (params) {
this.text = ko.observable(params && params.initialText || '');
},
template: 'Message: <input data-bind="value: text" /> '
+ '(length: <span data-bind="text: text().length"></span>)'
});
ko.applyBindings();
</script>
注意:在更现实的情况下,您通常会从外部文件加载组件视图模型和模板,而不是将它们硬编码到注册中。请参见示例和注册文档。
API
有两种方法可以使用组件绑定
- 速记语法
如果只传递一个字符串,它将被解释为组件名。然后注入命名组件,而不向其提供任何参数。例子
<div data-bind='component: "my-component"'></div>
简写值也可以被监控到。在本例中,如果组件绑定发生更改,则组件绑定将处理(dispose)旧组件实例,并注入新引用的组件。例子
<div data-bind='component: observableWhoseValueIsAComponentName'></div>
- 完整语法
要向组件提供参数,请传递具有以下属性的对象name
要注入的组件的名称。这也是可以观察到的。params
将传递给组件的对象。通常,这是一个包含多个参数的键值对象,通常由组件的viewmodel构造函数接收。
<div data-bind='component: {
name: "shopping-cart",
params: { mode: "detailed-list", items: productsList }
}'></div>
请注意,无论何时删除组件(要么是因为名称更改了observable,要么是因为封闭的控制流绑定删除了整个元素),删除的组件被释放( disposed)。
组件生命周期
1.组件加载器被要求提供 viewmodel 工厂和模板
通常,这是一个异步过程。 它可能涉及对服务器的请求。 对于API一致性,默认情况下Knockout确保加载过程作为异步回调完成,即使组件已经加载并缓存在内存中也是如此。 有关此内容以及如何允许同步加载的更多信息,请参阅控制同步/异步加载。
2.组件模板被克隆并注入容器元素
3.如果组件有一个视图模型 viewmodel ,则实例化它
4.视图模型 viewmodel 绑定到视图
5.组件被激活
6.组件被拆除,视图模型被放置
注意1:仅有模板组件
组件通常有视图模型,但不一定非得有。组件可以只指定模板。
在本例中,绑定组件视图的对象是传递给组件绑定的params对象。例子
ko.components.register('special-offer', {
template: '<div class="offer-box" data-bind="text: productName"></div>'
});
可以注入:
<div data-bind='component: {
name: "special-offer-callout",
params: { productName: someProduct.name }
}'></div>
或者,更方便地,作为自定义元素
<special-offer params='productName: someProduct.name'></special-offer>
使用无容器
<!-- ko component: {
name: "message-editor",
params: { initialText: "Hello, world!", otherParam: 123 }
} -->
<!-- /ko -->
标记传递给组件
附加组件绑定的元素可能包含进一步的标记。例如
<div data-bind="component: { name: 'my-special-list', params: { items: someArrayOfPeople } }">
<!-- Look, here's some arbitrary markup. By default it gets stripped out
and is replaced by the component output. -->
The person <em data-bind="text: name"></em>
is <em data-bind="text: age"></em> years old.
</div>
尽管该元素中的DOM节点将被删除,并且默认情况下不进行绑定,但它们不会丢失。相反,它们被提供给组件(在本例中是my-special-list
),组件可以按照自己的意愿将它们包含在输出中。
如果您想要构建表示容器UI元素的组件,例如网格、列表、对话框或选项卡集,这是非常有用的,因为这些元素需要将任意标记注入并绑定到公共结构中。有关自定义元素的完整示例,它也可以在没有使用上面所示语法的自定义元素的情况下工作。
<!-- This could be in a separate file -->
<template id="my-special-list-template">
<h3>Here is a special list</h3>
<ul data-bind="foreach: { data: myItems, as: 'myItem' }">
<li>
<h4>Here is another one of my special items</h4>
<!-- ko template: { nodes: $componentTemplateNodes, data: myItem } --><!-- /ko -->
</li>
</ul>
</template>
<my-special-list params="items: someArrayOfPeople">
<!-- Look, I'm putting markup inside a custom element -->
The person <em data-bind="text: name"></em> is
<em data-bind="text: age"></em> years old.
</my-special-list>
<script type="text/javascript">
ko.components.register("my-special-list", {
template: { element: "my-special-list-template" },
viewModel: function(params) {
this.myItems = params.items;
}
});
ko.applyBindings({
someArrayOfPeople: ko.observableArray([
{ name: "Lewis", age: 56 },
{ name: "Hathaway", age: 34 }
])
});
</script>