客户端洞察

Knockout 入门

John Papa

下载代码示例

John Papa
在目前的开发领域中,数据绑定是最常见的功能之一,Knockout JavaScript 库在 HTML 和 JavaScript 开发中引入了这些功能。声明性绑定语法的简易性以及与分离模式(例如 Model-View-ViewModel (MVVM))的无缝集成使得一般推拉探测任务大为简化,同时代码的维护和改进也更为容易。在此客户端洞察开篇栏目中,我将介绍 Knockout 的适用方案、入门知识并演示如何使用其基础功能。从 archive.msdn.microsoft.com/mag201202Client 下载的代码示例演示了如何使用声明性绑定、创建不同类型的绑定对象以及编写遵循良好分离模式(例如 MVVM)的以数据为中心的 JavaScript 代码。

开始使用

Knockout 由 Steve Sanderson 开发,是一种小型开源 JavaScript 库,具有 MIT 许可证。Knockoutjs.com 提供了 Knockout 支持的最新浏览器列表(目前支持包括 Internet Explorer 6 及更高版本、Firefox 2 及更高版本、Chrome、Opera 和 Safari 在内的所有主要浏览器)。你需要一些重要资源才能开始使用 Knockout 进行开发。请在开始时从 bit.ly/scmtAi 下载最新版本的 Knockout(目前为 2.0.0),并在你的项目中引用它。但是,如果你使用的是 Visual Studio 2010,强烈建议你安装并使用 NuGet Package Manager Visual Studio 扩展来下载 Knockout(以及任何其他可能使用的库),因为该软件可以管理多个版本并在新版本可用时发出通知。NuGet 将下载 Knockout 并将两个 JavaScript 文件放入项目的脚本文件夹中。建议使用缩小文件进行生产,并遵循命名约定 knockout-x.y.z.js,其中 x.y.z 依次为主版本号、次版本号和修订号。同时,还存在一个 knockout-x.y.x-debug.js 文件,其中包含人工可读格式的 Knockout 源代码。建议在学习 Knockout 和调试时参考此文件。

所有文件准备就绪之后,打开 HTML 页(如果你使用的是 ASP.NET MVC,则打开 Razor 文件),创建脚本并引用 knockout 库:

<script src="../scripts/knockout-2.0.0.js" type="text/javascript"></script>

无绑定

介绍 Knockout 最好的方法是首先查看如何在不使用 Knockout 的情况下编写代码以将数据从源对象推送到 HTML 元素(在可供下载的代码示例中,示例页面 01-without-knockout.html 提供了相关源代码)。 然后,我将演示如何使用 Knockout 完成相同工作。 我将从一些 HTML 目标元素入手,然后将值从源对象推送到这些 HTML 元素中:

<h2>Without Knockout</h2>

<span>Item number:</span><span id="guitarItemNumber"></span>

<br/>

<span>Guitar model:</span><input id="guitarModel"/>

<span>Sales price:</span><input  id="guitarSalesPrice"/>

如果你具有一个对象,你希望从该对象将数据推送到标准 HTML 元素,则可以使用 jQuery:

$(document).ready(function () {

  var product = {

    itemNumber: "T314CE",

    model: "Taylor 314ce",

    salePrice: 1199.95

    };

    $("#guitarItemNumber").text(product.itemNumber);

    $("#guitarModel").val(product.model);

    $("#guitarSalesPrice").val(product.salePrice);

});

此代码示例使用 jQuery 查找具有相应 ID 的 HTML 元素,并将其值设置为各个对应的对象属性。

在上述代码中,有三点需要注意。 第一,值是从源对象推送到 HTML 元素中,因此从源值到目标元素的每个映射都需要一行代码。 如果存在许多其他属性(或者如果存在数组和对象图),则代码很容易就会变得难于管理。 第二,如果源对象中的值发生更改,除非再次调用推送这些值的代码,否则 HTML 元素不会反映此更改。 第三,如果 HTML 元素(目标)中的值发生更改,此更改不会反映在基础源对象中。 当然,所有这些问题都可以通过大量代码来解决,不过,我将尝试使用数据绑定来解决这一问题。

使用 Knockout 可以重新编写相同 HTML:

<h2>With Knockout</h2>

<span Item number</span><span data-bind="text: itemNumber"></span>

<br/>

<span>Guitar model:</span><input data-bind="value: model"/>

<span>Sales price:</span><input data-bind="value: salePrice"/>

请注意,已使用 data-bind 属性取代了 id 属性。 调用 applyBindings 函数之后,Knockout 将指定对象(在本例中为“product”)绑定到页面。 这会将页面的数据上下文设置为 product 对象,意味着随后目标元素可以识别要绑定到的数据上下文的属性:

ko.applyBindings(product);

源对象中的值将推送到此页中的目标元素。 (Knockout 的所有函数都存在于其自己的命名空间中: 即 ko。) 目标元素和源对象之间的链接由 data-bind 属性定义。 在前面的示例中,Knockout 发现第一个范围标记的 data-bind 属性确定其文本值应绑定到 itemNumber 属性。 Knockout 随后将 product.itemNumber 属性的值推送到该元素。

你可以看到使用 Knockout 可以大大减少代码。 当属性和元素数目增长时,applyBindings 函数是所需的唯一 JavaScript 代码。 但是,这并未解决所有问题。 此示例在源发生更改时不会更新 HTML 目标,HTML 目标更改时也不会更新源对象。 为此,我们需要可观察量。

可观察量

Knockout 通过可观察量添加依赖项跟踪,这是一种在基础值发生更改时通知侦听程序的对象(这与 XAML 技术中的 INotifyPropertyChanged 接口概念相似)。 Knockout 通过使用名为 observable 的自定义函数包装对象属性来实现可观察量属性。 例如,不采用以下方法在对象中设置属性:

var product = {

  model: "Taylor 314ce"

}

可以使用 Knockout 将属性定义为可观察量属性:

var product = {

  model: ko.observable("Taylor 314ce")

}

将属性定义为可观察量之后,数据绑定实际已成型。 图 1 中所示的 JavaScript 代码演示了 Knockout 绑定到 HTML 元素的两个对象。 第一个对象 (data.product1) 使用简单对象文字定义其属性,而第二个对象 (data.product2) 将属性定义为 Knockout 可观察量。

图 1 使用和不使用可观察量

$(document).ready(function () {

  var data = {

    product1: {

      id: 1002,

      itemNumber: "T110",

      model: "Taylor 110",

      salePrice: 699.75

    },

    product2: {

      id: ko.observable(1001),

      itemNumber: ko.observable("T314CE"),

      model: ko.observable("Taylor 314ce"),

      salePrice: ko.observable(1199.95)

    }

  };

  ko.applyBindings(data);

});

图 2 中此示例的 HTML 展示了四组元素绑定。 第一个和第二个 div 标记包含绑定到非可观察量属性的 HTML 元素。 第一个 div 中的值发生更改时,请注意,其余内容不会发生更改。 第三个和第四个 div 标记包含绑定到可观察量属性的 HTML 元素。 请注意,如果第三个 div 中的值发生更改,则会更新第四个 div 中的元素。 你可以使用示例 02-observable.html 尝试此演示。

图 2 绑定到可观察量和非可观察量

<div>

  <h2>Object Literal</h2>

  <span>Item number</span><span data-bind="text: product1.itemNumber"></span>

  <br/>

  <span>Guitar model:</span><input data-bind="value: product1.model"/>

  <span>Sales price:</span><input data-bind="value: product1.salePrice"/>

</div>

<div>

  <h2>Underlying Source Object for Object Literal</h2>

  <span>Item number</span><span data-bind="text: product1.itemNumber"></span>

  <br/>

  <span>Guitar model:</span><span data-bind="text: product1.model"></span>

  <span>Sales price:</span><span data-bind="text: product1.salePrice"></span>

</div>

<div>

  <h2>Observables</h2>

    <span>Item number</span><span data-bind="text: product2.itemNumber"></span>

  <br/>

    <span>Guitar model:</span><input data-bind="value: product2.model"/>

    <span>Sales price:</span><input data-bind="value: product2.salePrice"/>

</div>

<div>

  <h2>Underlying Source Object for Observable Object</h2>

  <span>Item number</span><span data-bind="text: product2.itemNumber"></span>

  <br/>

    <span>Guitar model:</span><span data-bind="text: product2.model"></span>

    <span>Sales price:</span><span data-bind="text: product2.salePrice"></span>

</div>

(注意: Knockout 不要求使用可观察量属性。 如果你希望 DOM 元素仅接收一次值,而在源对象中的值发生更改时不会进行更新,简单对象足以满足需求。 但是,如果你希望源对象和目标 DOM 元素保持同步(即双向绑定),则需要考虑使用可观察量属性。)

内置绑定

迄今为止,上述示例演示了如何绑定到 Knockout 的文本和值绑定。 Knockout 具有多种内置绑定,使得将对象属性绑定到目标 DOM 元素更为简单。 例如,Knockout 发现文本绑定后,会设置 innerText 属性(使用 Internet Explorer)或等效属性(使用其他浏览器)。 使用文本绑定时,将覆盖以前的所有文本。 Knockout 具有多种内置绑定,图 3 提供了一些最常显示的绑定。 Knockout 联机文档左侧导航窗格中包含完整列表 (bit.ly/ajRyPj)。

图 3 常用 Knockout 绑定

示例 方案
text: model 将属性 (model) 绑定到目标元素的文本值。 通常用于只读元素,例如 spans。
visible: isInStock 将值属性 (isInStock) 绑定到目标元素的可见性。 该属性值的计算结果为 true 或 false。
value: price 将属性 (price) 的值绑定到目标元素。 通常与 input、select 和 textarea 元素一起使用。
css: className 将属性 (className) 的值绑定到目标元素。 通常用于设置或切换 DOM 元素的 css 类名。
checked: isInCart 将属性 (isInCart) 的值绑定到目标元素。 用于 checkbox 元素。
click: saveData 当用户单击 DOM 元素时,为绑定的 JavaScript 函数 (saveData) 添加事件处理程序。 可用于任意 DOM 元素,不过通常用于 button、input 和 a 元素。
attr: {src: photoUrl, alt: name} 将 DOM 元素的任意指定属性绑定到源对象。 通常用于其他内置绑定未涉及到的情形,例如与 img 标记的 src 属性一起使用。

可观察量数组

现在,前面的示例让你通过实践来初步了解了 Knockout,现在让我们看看更切实可行的基础分层数据示例。 Knockout 支持多种类型的绑定,包括绑定到简单属性(如前面的示例所示)、绑定到 JavaScript 数组、计算绑定和自定义绑定(将在以后关于 Knockout 的文章中介绍)。 下一个示例演示了如何使用 Knockout 将产品对象数组绑定到列表(如图 4 中所示)。

Binding to an Observable Array
图 4 绑定到可观察量数组

处理对象图和数据绑定时,将页面需要的所有数据和函数封装到单一对象中非常有用。 这通常称为 MVVM 模式的 ViewModel。 在本例中,View 是 HTML 页及其 DOM 元素。 Model 是产品数组。 ViewModel 将 Model 粘合到 View;使用的粘合剂为 Knockout。

使用 observableArray 函数设置产品数组。 这与 XAML 技术中的 ObservableCollection 类似。 由于产品属性是可观察量数组,每次在数组中添加或删除项目时,将通知目标元素并在 DOM 中添加或删除项目,如此处所示:

var showroomViewModel = {

  products: ko.observableArray()

};

showroomViewModel 是根对象,它将数据绑定到目标元素。 它包含来自 JSON 形式的数据服务的产品列表。 加载产品列表的函数是 showroomViewModel.load 函数,如图 5 中所示,其中包含了设置 showroomViewModel 对象的剩余 JavaScript 部分(在 03-observableArrays.html 中可以找到此示例的完整源和示例数据)。 加载函数遍历示例产品数据,使用 Product 函数创建新的产品对象,然后将其推送到可观察量数组。

图 5 定义要绑定的数据

var photoPath = "/images/";

function Product () {

  this.id = ko.observable();

  this.salePrice = ko.observable();

  this.listPrice = ko.observable();

  this.rating = ko.observable();

  this.photo = ko.observable();

  this.itemNumber = ko.observable();

  this.description = ko.observable();

  this.photoUrl = ko.computed(function () {

    return photoPath + this.photo();

  }, this);

};

var showroomViewModel = {

  products: ko.observableArray()

};

showroomViewModel.load = function () {

  this.products([]); // reset

  $.each(data.Products, function (i, p) {

    showroomViewModel.products.push(new Product()

      .id(p.Id)

      .salePrice(p.SalePrice)

      .listPrice(p.ListPrice)

      .rating(p.Rating)

      .photo(p.Photo)

      .itemNumber(p.ItemNumber)

      .description(p.Description)

      );

  });

};

ko.applyBindings(showroomViewModel);

虽然产品的所有属性均使用可观察量定义,但不一定必须使用可观察量。 例如,如果这些属性为只读,并且在源更改时无需更新目标或应刷新整个容器,则可以为纯粹的属性。 但是,如果源更改时需要刷新属性或者在 DOM 中编辑,则可观察量是最佳选择。

Product 函数将其所有属性定义为 Knockout 可观察量(photoUrl 除外)。 Knockout 绑定数据时,可以访问产品数组属性,这样可以方便地使用标准函数(例如 length)来显示当前有多少个项进行了数据绑定:

<span data-bind="text: products().length"></span>

流程控制绑定

产品数组可以数据绑定到 DOM 元素,在 DOM 元素中,产品数组可用作匿名模板来显示列表。 下列 HTML 显示的 ul 标记使用 foreach 流程控制来绑定到产品:

<ul data-bind="foreach:products">

  <li class="guitarListCompact">

    <div class="photoContainer">

      <img data-bind="visible: photoUrl, attr: { src: photoUrl }"

        class="photoThumbnail"></img>

    </div>

    <div data-bind="text: salePrice"></div>

  </li>

</ul>

ul 标记内部的元素将用作各个产品的模板。 在 foreach 中,数据上下文也将从根对象 showroomViewModel 更改为各个产品。 这正是内部 DOM 元素可以直接绑定到产品的 photoUrl 和 salePrice 属性的原因。

主要有四个流程控制绑定: foreach、if、ifnot 和 with。 使用这些控制绑定可以声明性定义流程控制逻辑,而无需创建指定的模板。 当 if 流程控制绑定后跟的条件为 true 时,或者 ifnot 绑定后跟的条件的计算结果为 false 时,将绑定并显示其块内部的内容,如此处所示:

<div data-bind="if:onSale">

  <span data-bind="text: salePrice"></span>

</div>

with 绑定将数据上下文更改为你指定的任意对象。 这在深入到带有多个父/子关系的对象图或者页面中的独特 ViewModels 时尤为有用。 例如,如果存在一个绑定到页面的 sale ViewModel 对象,并且该对象具有子对象 customer 和 salesPerson,则 with 绑定可用于使绑定更易于阅读和维护,如此处所示:

<div data-bind="with:customer">

  <span data-bind="text: name"></span><br/>

  <span data-bind="text: orderTotal"></span>

</div>

<div data-bind="with:salesPerson">

  <span data-bind="text: employeeNum"></span><br/>

  <span data-bind="text: name"></span>

</div>

计算可观察量

你可能已注意到,Product 函数将 photoUrl 定义为一种特殊类型的计算属性。 ko.computed 定义绑定函数,绑定函数计算数据绑定操作的值。 如果计算属性求值时所依赖的任何可观察量发生更改,计算属性会自动更新。 当未在源对象中的具体值中明确表示值时,此功能尤为有用。 除了创建 URL 之外,另一个常见示例是使用 firstName 和 lastName 属性创建 fullName 属性。

(注意: 以前的 Knockout 版本将计算属性作为 dependentObservable 引用。 两者在 Knockout 2.0.0 中仍然适用,但建议使用较新的计算函数。)

计算属性接受用于计算值的函数,并接受表示绑定要附加到的对象。 第二个参数非常重要,因为 JavaScript 对象文字没有任何方式可以引用自身。 在图 5 中,传入 this 关键字(表示 showroomViewModel),因此依赖可观察量的函数可以用它来获取 photo 属性。 如果未传入 this 关键字,将不会定义 photo 函数,并且计算将无法生成所需的 URL:

this.photoUrl = ko.computed(function () {

  return photoPath +  photo();  // photo() will be undefined

});

了解基础绑定属性

本专栏帮助你掌握使用 Knockout JavaScript 库进行数据绑定的入门知识。 Knockout 最重要的方面是了解基础绑定属性: 可观察量、可观察量数组和计算可观察量。 通过这些可观察量,你可以使用稳固分离模式创建可靠的 HTML 应用程序。 本文还介绍了一些较常用的内置绑定类型并演示了流程控制绑定。 不过,Knockout 还支持很多功能。 下次我将更详细地介绍内置绑定。

John Papa 曾任 Microsoft Silverlight 和 Windows 8 团队推广专家,他主持的 Silverlight 电视秀节目深受观众欢迎。 他在全球参与了 BUILD、MIX、PDC、Tech•Ed、Visual Studio Live! 和 DevConnections 活动的主题演讲和研讨会。 Papa 同时也是 Visual Studio 杂志的专栏作家 (Papa's Perspective) 以及 Pluralsight 培训视频作者。 有关他的情况,请访问 Twitter 上的 twitter.com/john_papa

衷心感谢以下技术专家对本文的审阅:Steve Sanderson