# 八、制作 knockout 动画

knockout. js 是*不是*一个动画库。每当底层数据发生变化时，所有 knockout. js 的自动更新都会立即应用于*。为了动画化它的任何变化，我们需要深入到敲除. js 的内部，并使用另一个像 jQuery 或 MooTools 这样的 JavaScript 框架手动创建动画化的转换。本章坚持使用 jQuery 的动画例程，但是所展示的概念也适用于其他动画库。*

 *## 购物车的返回

在本章中，我们将回到购物车示例的简化版本。用以下内容创建一个新的 HTML 文件。我们不会发出任何 AJAX 请求，所以请随意将它放在您计算机的任何地方。然而，我们将使用 jQuery 的动画例程，所以一定要包含一个到 jQuery 库副本的链接。

```js
    <html lang='en'> <head>   <title>Animating Knockout.js</title>   <meta charset='utf-8' />   <link rel='stylesheet' href='style.css' /> </head> <body>
      <h1>Animating Knockout.js</h1>
      <table>
        <thead><tr>
          <th>Product</th>
          <th>Price</th>
          <th></th>
        </tr></thead>
        <tbody data-bind='foreach: items'>
          <tr>
            <td data-bind='text: name'></td>
            <td data-bind='text: price'></td>
            <td><button data-bind='click: $root.removeProduct'>Remove</button></td>
          </tr>
        </tbody>
      </table>

      <button data-bind='click: addProduct'>Add Beer</button>

      <script type='text/javascript' src='knockout-2.1.0.js'></script>
      <script src='jquery-1.7.2.js'></script>
      <script type='text/javascript'>     function Product(name, price, tags, discount, details) {       this.name = ko.observable(name);       this.price = ko.observable(price);     }     function ShoppingCart() {       var self = this;       this.instructions = ko.observable("");       this.hasInstructions = ko.observable(false);              this.items = ko.observableArray([         new Product("Beer", 10.99),         new Product("Brats", 7.99),         new Product("Buns", 1.49)       ]);              this.addProduct = function() {         this.items.push(new Product("More Beer", 10.99));       };
           this.removeProduct = function(product) {         self.items.destroy(product);       };            };     ko.applyBindings(new ShoppingCart());   </script> </body> </html>

```

希望现在这些都是回顾。我们有一个包含一堆产品的可观测数组，一个显示每个产品的`foreach`绑定，以及一个向购物车添加更多商品的按钮。

## 列表回调

gentle . js 本身是一个强大的用户界面库，但是一旦您将其与 jQuery 或 MooTools 等框架的动画功能相结合，您就可以用最少的标记创建真正令人惊叹的 ui。首先，我们将看一下动画列表，然后下一节介绍一种更通用的动画视图组件的方法。

`foreach`绑定有两个名为`beforeRemove`和`afterAdd`的回调。这些功能分别在项目从列表中移除之前或添加到列表之后执行。这给了我们一个机会，在敲除操作 DOM 之前制作每个项目的动画。向`<tbody>`元素添加回调，如下所示:

```js
        <tbody data-bind='foreach: {data: items,       beforeRemove: hideProduct,       afterAdd: showProduct}'>

```

现在，我们的`foreach`绑定不再是一个属性，而是将一个对象文字作为其参数。参数的`data`属性指向您想要渲染的数组，`beforeRemove`和`afterAdd`属性指向所需的回调函数。接下来，我们应该在`ShoppingCart`视图模型上定义这些回调:

```js
          this.showProduct = function(element) {         if (element.nodeType === 1) {           $(element).hide().fadeIn();         }       };
           this.hideProduct = function(element) {         if (element.nodeType === 1) {          $(element).fadeOut(function() { $(element).remove(); });         }       };

```

`showProduct()`回调使用 jQuery 让新列表项逐渐淡入，`hideProduct()`回调淡出，然后从 DOM 中移除。这两个函数都将受影响的 DOM 元素作为它们的第一个参数(在本例中，它是一个`<tr>`元素)。条件语句确保我们使用的是完整的元素，而不仅仅是文本节点。

最终的结果应该是列出那些可以平稳地进出列表的项目。当然，您可以自由使用 jQuery 的任何其他转换，或者在任一回调中执行自定义后处理。

## 自定义绑定

`foreach`回调对于动画列表非常有用，但是不幸的是其他绑定不提供这个功能。因此，如果我们想要动画用户界面的其他部分，我们必须创建*自定义*绑定，将动画嵌入其中。

自定义绑定的工作方式就像敲除. js 的默认绑定一样。例如，考虑以下表单域:

```js
      <div>
        <p>
          <input data-bind='checked: hasInstructions'              type='checkbox' />
          Requires special handling instructions
        </p>
        <div>
          <textarea data-bind='visible: hasInstructions,                            value: instructions'>
          </textarea>
        </div>
      </div>

```

该复选框充当了`<textarea>`的切换开关，但是由于我们使用的是`visible`绑定，knockout. js 会突然在 DOM 中添加或删除它。为了给`<textarea>`提供一个平稳的过渡，我们将创建一个名为`visibleFade`的自定义绑定:

```js
          <textarea data-bind='visibleFade: hasInstructions,                            value: instructions'>

```

当然，在我们将自定义绑定添加到 knockout. js 之前，这是行不通的，我们可以通过将定义绑定的对象添加到`ko.bindingHandlers`来实现，如下面的代码示例所示。这也恰好是定义所有内置绑定的地方。

```js
        ko.bindingHandlers.visibleFade = {       init: function(element, valueAccessor) {         var value = valueAccessor();         $(element).toggle(value());       },       update: function(element, valueAccessor) {         var value = valueAccessor();         value() ? $(element).fadeIn() : $(element).fadeOut();       }     }

```

`init`属性指定了第一次遇到绑定时要调用的函数。这个回调应该定义视图组件的初始状态，并执行必要的设置操作(例如，注册事件侦听器)。对于`visibleFade`，我们所要做的就是根据视图模型的状态显示或隐藏元素。我们使用 jQuery 的`toggle()`方法实现了这一点。

`element`参数是被绑定的 DOM 元素，`valueAccessor`是将返回所讨论的视图模型属性的函数。在我们的示例中，`element`引用了`<textarea>`，而`valueAccessor()`返回了对`hasInstructions`的引用。

`update`属性指定了一个函数，每当相关的可观测到的变化时执行，我们的回调使用`hasInstructions`的值将`<textarea>`转换到适当的方向。请记住，您需要调用可观测值来获取其当前值(即`value()`，而不是`value`)。然而，如果`hasInstructions`是一个普通的 JavaScript 属性而不是一个可观测的属性，情况就不是这样了。

## 摘要

在这一章中，我们发现了两种制作 Quicken . js 视图组件动画的方法。首先，我们向`foreach`绑定添加了回调方法，这让我们可以将项目的添加和移除委托给用户定义的函数。这让我们有机会将 jQuery 的动画转换集成到我们的 knockout. js 模板中。然后，我们探索了自定义绑定作为动画任意元素的手段。

本章介绍了自定义绑定的一个常见用例，但它们绝不仅限于动画用户界面组件。自定义绑定还可以用来过滤收集的数据，监听自定义事件，或者创建可重用的小部件，如网格和分页内容。如果你能把一个行为封装成一个`init`和一个`update`函数，你就能把它变成一个自定义绑定。*