jQuery-设计模式-全-

jQuery 设计模式(全)

原文:zh.annas-archive.org/md5/9DBFD51895CA93BE96AC02124FF5B7E1

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

自 2006 年推出以来,jQuery 库已经使 DOM 遍历和操作变得更加容易。这导致了具有越来越复杂用户交互的 Web 页面的出现,从而促进了 Web 作为支持大型应用程序实现的平台的成熟。

本书提供了一系列使 Web 应用程序实现更高效的最佳实践。此外,我们将分析可以应用于 Web 开发的计算机科学中最重要的设计模式。通过这种方式,我们将学习如何利用其他编程领域中广泛使用和测试的技术,这些技术最初是作为模拟复杂问题解决方案的通用方法创建的。

在《jQuery 设计模式》中,我们将分析各种设计模式在 jQuery 实现中的应用以及它们如何用于改进我们的实现组织。通过采用本书中展示的设计模式,您将能够创建更有组织的实现,更快地解决大问题类别。此外,当开发团队使用时,它们可以提高团队之间的沟通并导致同质化实现,其中代码的每个部分都易于被其他人理解。

本书涵盖内容

第一章, jQuery 和复合模式复习,将教读者如何通过分析它们在 jQuery 本身实现中的使用来编写代码,使用复合模式和方法链接(流畅接口)。它还演示了与 jQuery 返回的复合集合对象良好配对的迭代器模式。

第二章,观察者模式,将教您如何使用观察者模式响应用户操作。它还演示了如何使用事件委托作为一种减少处理动态注入页面元素的代码的内存消耗和复杂性的方法。最后,它将教您如何发出和监听自定义事件,以实现更大的灵活性和代码解耦。

第三章,发布/订阅模式,将教您如何利用发布/订阅模式创建一个中心点来发出和接收应用程序级事件,作为解耦代码和业务逻辑与用于呈现的 HTML 之间的一种方式。

第四章, 用模块模式进行分而治之, 展示并比较了行业中最常用的模块模式。它将教会你如何使用命名空间来将你的应用程序结构化为小的独立模块,遵循关注点分离原则,从而实现可扩展的实现。

第五章, 外观模式, 将教会你如何使用外观模式将复杂的 API 包装成更适合你应用程序需求的简单 API。它还演示了如何改变应用程序的部分,同时保持相同的模块级 API,避免影响其余的实现。

第六章, 建造者和工厂模式, 解释了建造者模式和工厂模式的概念和区别。它将教会你何时以及如何使用它们,以改善代码的清晰度,通过将生成复杂结果的过程抽象成单独的专用方法。

第七章, 异步控制流模式, 将解释 jQuery 的 Deferred 和 Promise API 是如何工作的,并将它们与经典的回调模式进行比较。你将学习如何使用 Promises 来控制异步程序的执行,让它们以顺序或并行的方式运行。

第八章, 模拟对象模式, 教会你如何创建和使用模拟对象和服务作为一种简化应用程序开发的方式,并在所有部分完成之前提前感受到其功能。

第九章, 客户端模板化, 展示如何使用 Underscore.js 和 Handlebars.js 模板库作为创建复杂 HTML 结构的更好更快的方式。通过这一章节,你将了解它们的惯例,评估它们的特点,并找到最符合你口味的那一个。

第十章, 插件和小部件开发模式, 介绍了 jQuery 插件开发的基本概念和惯例,并分析了最常用的设计模式,使你能够识别并使用最适合任何用例的模式。

第十一章 优化模式,指导您使用最佳提示创建高效且健壮的实现。您将能够将本章用作改进应用程序性能并降低内存消耗的最佳实践的检查表,然后将其移到生产环境。

本书所需内容

为了运行本书中的示例,您需要在系统上安装 Web 服务器来提供代码文件。例如,您可以使用 Apache、IIS 或 NGINX。为了使 Apache 的安装过程更加简单,您可以使用更完整的开发环境解决方案,例如 XAMPP 或 WAMP Server。

就技术熟练度而言,本书假定您已经有一些使用 jQuery、HTML、CSS 和 JSON 的经验。本书中的所有代码示例都使用 jQuery v2.2.0,并且一些章节还讨论了在 jQuery v1.12.0 中的相应实现,这可以在需要支持旧版浏览器的情况下使用。

本书适合谁

这本书面向现有的 jQuery 开发者或想将自己的技能和理解水平提升到高级水平的新开发者。这是一个详细介绍了如何将各种行业标准模式应用于 jQuery 应用程序的入门,以及一组最佳实践,它可以帮助大型团队协作并创建组织良好、可扩展的实现。

约定

在本书中,您会发现一些文本样式,用于区分不同类型的信息。以下是这些样式的一些示例及其含义的解释。

文本中的代码词、文件夹名称、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄显示如下:“在上述 CSS 代码中,我们首先为 boxboxsizerclear CSS 类定义了一些基本样式。”

代码块设置如下:

$.each([3, 5, 7], function(index){
    console.log(this + 1 + '!');
});

当我们希望引起您对代码块的特定部分的注意时,相关行或项目将以粗体设置:

$('#categoriesSelector').change(function() { 
    var $selector = $(this); 
    var message = { categoryID: $selector.val() }; 
 broker.trigger('dashboardCategorySelect', [message]); 
});

我们正在遵循 Google JavaScript 样式指南,除了使用四个空格缩进外,以改善书中代码的可读性。简而言之,我们将大括号放在顶部,并使用单引号表示字符串字面值。

注意

有关 Google JavaScript 样式指南的更多信息,请访问以下 URL:google.github.io/styleguide/javascriptguide.xml

任何命令行输入或输出都以如下形式书写:

npm install jquery

新术语重要单词以粗体显示。屏幕上看到的词语,例如菜单或对话框中的词语,以如下形式出现在文本中:“返回的 jQuery 对象 是一个类似数组的对象,它充当包装对象并携带检索到的元素的集合。”

注意

警告或重要提示会出现在这样的框中。

提示

提示和技巧会出现在这样。

第一章:jQuery 和复合模式复习。

直到 Web 2.0 时代开始,Web 只是基于文档的媒体,它所提供的仅仅是连接不同页面/文档和客户端脚本编写,大多数情况下仅限于表单验证。到 2005 年,Gmail 和 Google 地图发布了,JavaScript 证明了自己是大型企业用于创建大规模应用程序并提供丰富用户界面交互的语言。

尽管 JavaScript 自发布以来几乎没有什么变化,但企业界对网页应该具备的功能期望发生了巨大变化。从那时起,Web 开发人员需要提供复杂的用户交互,并最终,"Web 应用程序" 这个术语出现在市场上。因此,开始变得明显,他们应该创建一些代码抽象,定义一些最佳实践,并采用计算机科学提供的所有适用的 设计模式。JavaScript 作为企业级应用程序的广泛采用帮助了语言的发展,随着 EcmaScript2015/EcmaScript6ES6)规范的发布,语言得以扩展,以便更轻松地利用更多的设计模式。

2006 年 8 月,John Resig 在 jquery.com 首次发布了 jQuery 库,旨在创建一个方便的 API 来定位 DOM 元素。从那时起,它已成为 Web 开发人员工具包的一个组成部分。jQuery 在其核心中使用了几种设计模式,并通过提供的方法试图敦促开发人员使用它们。复合模式是其中之一,它通过非常核心的 jQuery() 方法向开发人员公开,该方法用于 DOM 遍历,这是 jQuery 库的一个亮点。

在本章中,我们将:

  • 通过 jQuery 进行 DOM 脚本编写的复习。

  • 介绍复合模式。

  • 看看 jQuery 如何使用复合模式。

  • 讨论 jQuery 相对于纯 JavaScript DOM 操作所带来的优势。

  • 介绍迭代器模式。

  • 在一个示例应用中使用迭代器模式。

jQuery 和 DOM 脚本编写。

通过 DOM 脚本编写,我们指的是在浏览器加载后修改或操作网页元素的任何过程。DOM API 是一种 JavaScript API,于 1998 年标准化,它为网页开发人员提供了一组方法,允许在加载和解析网页的 HTML 代码后操作浏览器创建的 DOM 树元素。

注意。

要了解有关 文档对象模型DOM)及其 API 的更多信息,您可以访问 developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Introduction

通过在他们的 JavaScript 代码中利用 DOM API,web 开发者可以操纵 DOM 的节点,并向页面添加新元素或删除现有元素。最初 DOM 脚本的主要用例仅限于客户端表单验证,但随着时间的推移和 JavaScript 获得企业界的信任,开始实现更复杂的用户交互。

jQuery 库的初始版本于 2006 年 8 月首次发布,它试图简化 web 开发者遍历和操纵 DOM 树的方式。其主要目标之一是提供抽象,以产生更短、更易读、更不容易出错的代码,同时确保跨浏览器的互操作性。

jQuery 遵循的这些核心原则在其主页中清晰可见,它将自己呈现为:

…一个快速、小巧且功能丰富的 JavaScript 库。它通过一个易于使用的 API,简化了 HTML 文档遍历和操纵、事件处理、动画和 Ajax,适用于众多浏览器。jQuery 结合了多功能性和可扩展性,改变了数百万人编写 JavaScript 的方式。

jQuery 从一开始提供的抽象 API,以及不同的设计模式是如何编排的,导致在 web 开发者中得到了广泛的接受。因此,根据多个来源(例如 BuiltWith.com (trends.builtwith.com/javascript/jQuery)),全球访问量最高的网站中有超过 60% 的网站引用了 jQuery 库。

使用 jQuery 操纵 DOM

为了对 jQuery 进行复习,我们将通过一个示例网页进行一些简单的 DOM 操作。在这个例子中,我们将加载一个简单结构的页面,最初看起来像下图所示:

使用 jQuery 操纵 DOM

我们将使用一些 jQuery 代码来更改页面的内容和布局,并且为了使其效果清晰可见,我们将设置它在页面加载后约700 milliseconds运行。我们的操作结果将如下图所示:

使用 jQuery 操纵 DOM

现在让我们回顾一下前面示例所需的 HTML 代码:

<!DOCTYPE html> 
<html> 
  <head> 
    <title>DOM Manipulations</title> 
    <link rel="stylesheet" type="text/css" href="dom-manipulations.css">
  </head> 
  <body> 
    <h1 id="pageHeader">DOM Manipulations</h1> 

    <div class="boxContainer"> 
      <div> 
        <p class="box"> 
          Doing DOM Manipulations is easy with JS! 
        </p> 
      </div> 
      <div> 
        <p class="box"> 
          Doing DOM Manipulations is easy with JS! 
        </p> 
      </div> 
      <div> 
        <p class="box"> 
          Doing DOM Manipulations is easy with JS! 
        </p> 
      </div> 
    </div> 

    <p class="box"> 
      Doing DOM Manipulations is easy with JS! 
    </p> 
    <p class="box"> 
      Doing DOM Manipulations is easy with JS! 
    </p>

    <script type="text/javascript" src="img/jquery-2.2.0.min.js"></script>
    <script type="text/javascript" src="img/jquery-dom-manipulations.js"></script>
  </body>
</html>

使用的 CSS 代码非常简单,只包含三个 CSS 类,如下所示:

.box {
    padding: 7px 10px;
    border: solid 1px #333;
    margin: 5px 3px;
    box-shadow: 0 1px 2px #777;
}

.boxsizer {
    float: left;
    width: 33.33%;
}

.clear { clear: both; }

前述代码在浏览器中打开并在执行我们的 JavaScript 代码之前,页面看起来像第一个图示所示。在前述 CSS 代码中,我们首先为 boxboxsizerclear CSS 类定义了一些基本样式。box 类通过一些填充、一条细边框、周围一些间距和在元素下方创建一个小阴影来为页面中的相关元素添加样式,使它们看起来像一个盒子。boxsizer 类将使用它的元素的宽度设置为其父元素的 1/3,并创建一个三列布局。最后,clear 类将用于元素作为列布局的断点,以使其后的所有元素都位于其下方。boxsizerclear 类最初未被 HTML 代码中定义的任何元素使用,但会在我们将在 JavaScript 中进行的 DOM 操作之后使用。

在我们的 HTML 的 <body> 元素中,我们最初定义了一个带有 ID pageHeader<h1> 标题元素,以便通过 JavaScript 轻松选择。紧接着,在它下面,我们定义了五个段落元素 (<p>),具有 box 类,前三个元素被包裹在三个 <div> 元素中,然后再包裹在另一个具有 boxContainer 类的 <div> 元素中。

到达我们的两个 <script> 标签时,我们首先从 jQuery CDN 引入了对 jQuery 库的引用。有关更多信息,您可以访问 code.jquery.com/。在第二个 <script> 标签中,我们引用了带有所需代码的 JavaScript 文件,例如:

setTimeout(function() {
    $('#pageHeader').css('font-size', '3em');

    var $boxes = $('.boxContainer .box');
    $boxes.append(
      '<br /><br /><i>In case we need simple things</i>.');
    $boxes.parent().addClass('boxsizer');

    $('.boxContainer').append('<div class="clear">');
}, 700);

我们所有的代码都包装在一个 setTimeout 调用中以延迟其执行,根据之前描述的用例。setTimeout 函数调用的第一个参数是一个匿名函数,它将在定时器 700 毫秒过期后执行,如第二个参数中定义的那样。

在我们匿名回调函数的第一行,我们使用 jQuery 的 $() 函数遍历 DOM 并定位 ID 为 pageHeader 的元素,并使用 css() 方法将其 font-size 增加到 3em。接下来,我们向 $() 函数提供了一个更复杂的 CSS 选择器,来定位所有具有 box 类的元素,这些元素是具有 boxContainer 类的元素的后代,然后将结果存储在名为 $boxes 的变量中。

提示

变量命名约定

在开发者中使用命名约定来命名持有特定类型对象的变量是一种常见做法。使用这种约定不仅有助于你记住变量持有的内容,还能使你的代码更易于其他团队成员理解。在 jQuery 开发者中,当变量存储了 $() 函数的结果(也称为 jQuery 集合对象)时,使用以 "$" 符号开头的变量名是常见的。

在获取我们感兴趣的box元素之后,我们在每个元素的末尾添加两个换行空格和一些额外的斜体文本。然后,我们使用$boxes变量遍历 DOM 树,使用parent()方法上升一个级别。parent()方法返回一个不同的 jQuery 对象,其中包含我们最初选择的框的父<div>元素,然后我们链式调用addClass()方法将它们分配给boxsizer CSS 类。

小贴士

如果您需要遍历所选元素的所有父节点,则可以使用$.fn.parents()方法。如果您只需要找到与给定 CSS 选择器匹配的第一个祖先元素,请考虑改用$.fn.closest()方法。

最后,由于boxsizer类使用浮动来实现三列布局,我们需要清除boxContainer中的浮动。再次,我们使用简单的.boxContainer CSS 选择器和$()函数遍历 DOM。然后,我们调用.append()方法创建一个带有.clear CSS 类的新<div>元素,并将其插入到boxContainer的末尾。

700 毫秒后,我们的 jQuery 代码将完成,结果是之前显示的三列布局。在其最终状态下,我们的boxContainer元素的 HTML 代码如下所示:

<div class="boxContainer"> 
 <div class="boxsizer"> 
    <p class="box"> 
      Doing DOM Manipulations is easy with JS! 
 <br><br><i>In case we need simple things</i>. 
    </p> 
  </div> 
 <div class="boxsizer"> 
    <p class="box"> 
      Doing DOM Manipulations is easy with JS! 
 <br><br><i>In case we need simple things</i>. 
    </p> 
  </div> 
 <div class="boxsizer"> 
    <p class="box"> 
      Doing DOM Manipulations is easy with JS! 
 <br><br><i>In case we need simple things</i>. 
    </p> 
  </div> 
 <div class="clear"></div> 
</div> 

方法链和流畅接口

实际上,在上面的示例中,我们还可以进一步将所有三个与框相关的代码语句合并为一个,其效果如下所示:

$('.boxContainer .box') 
  .append('<br /><br /><i>In case we need simple things</i>.') 
  .parent() 
  .addClass('boxsizer');

这种语法模式被称为方法链,并且被 jQuery 和 JavaScript 社区广泛推荐。方法链是流畅接口的面向对象实现模式的一部分,其中每个方法将其指令上下文传递给后续方法。

大多数适用于 jQuery 对象的 jQuery 方法也会返回相同或新的 jQuery 元素集合对象。这使我们能够链式调用多个方法,不仅使代码更易读和表达,而且减少了所需的变量声明。

组合模式

组合模式的关键概念是使我们能够像处理单个对象实例一样处理对象集合。通过在集合上使用方法来操作组合将导致将操作应用于其每个部分。这样的方法可以成功应用,而不管组合集合中包含的元素数量如何,甚至当集合不包含元素时也可以。

另外,组合集合的对象不一定需要提供完全相同的方法。组合对象可以只公开集合对象中对象之间共同的方法,或者可以提供一个抽象的 API,并适当处理每个对象的方法差异。

让我们继续探讨 jQuery 公开的直观 API 如何受到组合模式的高度影响。

jQuery 如何使用组合模式

组合模式是 jQuery 架构的一个组成部分,并且从 $() 函数的核心自身应用。 对 $() 函数的每次调用都会创建并返回一个元素集合对象,这通常简称为 jQuery 对象。 这正是我们看到组合模式的第一个原则的地方; 实际上,$() 函数不是返回单个元素,而是返回一组元素。

返回的 jQuery 对象是一个类似数组的对象,充当包装对象并携带检索到的元素集合。 它还公开了一些额外的属性,如下所示:

  • 检索到的元素集合的 length

  • 对象构造的 context

  • $() 函数调用中使用的 CSS selector

  • 在链式调用方法后如果我们需要访问先前的元素集合,则有一个 prevObject 属性

提示

简单的类似数组对象定义

类似数组的对象是具有数字 length 属性和相应数量的属性的 JavaScript 对象 { },具有连续的数字属性名称。 换句话说,具有 length == 2 属性的类似数组对象预计也应该定义两个属性 "0""1"。 给定上述属性,类似数组的对象允许您使用简单的 for 循环访问它们的内容,通过利用 JavaScript 的括号属性访问器的语法:

for (var i = 0; i < obj.length; i++) { 
  console.log(obj[i]); 
}

我们可以轻松地尝试使用 $() 函数返回的 jQuery 对象,并通过使用我们喜爱的浏览器的开发者工具检查上述属性。 要在大多数浏览器上打开开发者工具,我们只需要在 Windows 和 Linux 上按下 F12,或在 Mac 上按 Cmd + Opt + I,然后我们可以在控制台中发出一些 $() 调用并单击返回的对象以检查它们的属性。

在下图中,我们可以看到在之前示例中使用的 $('#pageHeader') 调用的结果在 Firefox 开发者工具中的样子:

jQuery 如何使用组合模式

$('.boxContainer .box') 调用的结果如下:

jQuery 如何使用组合模式

jQuery 使用类似数组的对象作为返回元素的包装器,从而使其能够公开一些额外的适用于返回的集合的方法。这是通过原型继承 jQuery.fn 对象来实现的,导致每个 jQuery 对象也可以访问 jQuery 提供的所有方法。这完成了组合模式,该模式提供了适用于集合的方法,这些方法适用于集合的每个成员。因为 jQuery 使用类似数组的对象具有原型继承,所以这些方法可以轻松地作为每个 jQuery 对象的属性访问,就像本章开头的示例中所示:$('#pageHeader').css('font-size', '3em');。此外,jQuery 还为其 DOM 操作代码添加了一些额外的好处,遵循更小和更不容易出错的代码的目标。例如,当使用 jQuery.fn.html() 方法更改已包含子元素的 DOM 节点的内部 HTML 时,jQuery 首先尝试删除与子元素关联的任何数据和事件处理程序,然后再将它们从页面中删除并附加所提供的 HTML 代码。

让我们看一下 jQuery 如何实现这些适用于集合的方法。对于这个任务,我们可以从 jQuery 的 GitHub 页面下载并查看源代码(github.com/jquery/jquery/releases),或者使用类似 jQuery 源代码查看器这样的工具,该工具可在james.padolsey.com/jquery找到。

注意

根据您使用的版本,您可能会在某种程度上获得不同的结果。在编写本书时,作为参考的最新稳定版 jQuery 版本是 v2.2.0。

展示适用于集合的方法如何实现的最简单方法之一是 jQuery.fn.empty()。您可以通过搜索 "empty:" 或使用 jQuery 源代码查看器并搜索 "jQuery.fn.empty" 来轻松找到它在 jQuery 源代码中的实现。使用其中任一种方式都会带我们到以下代码:

empty: function() { 
  var elem, i = 0; 

  for ( ; ( elem = this[ i ] ) != null; i++ ) {
    if ( elem.nodeType === 1 ) { 
      // Prevent memory leaks 
      jQuery.cleanData( getAll( elem, false ) ); 

      // Remove any remaining nodes 
      elem.textContent = ""; 
    } 
  } 

  return this; 
}

如您所见,代码一点也不复杂。jQuery 使用简单的 for 循环遍历集合对象的所有项(称为 this,因为我们在方法实现内部),对于集合的每个项,即元素节点,它都使用 jQuery.cleanData() 辅助函数清除任何 data-* 属性值,然后立即将其内容设置为空字符串。

注意

关于不同指定节点类型的更多信息,请访问developer.mozilla.org/en-US/docs/Web/API/Node/nodeType

与普通 DOM API 相比的优势

为了清楚地展示复合模式提供的好处,我们将在不使用 jQuery 提供的抽象的情况下重新编写我们最初的示例。通过仅使用普通 JavaScript 和 DOM API,我们可以编写一个等效的代码,如下所示:

setTimeout(function() { 
  var headerElement = document.getElementById('pageHeader'); 
  if (headerElement) { 
    headerElement.style.fontSize = '3em'; 
  } 
  var boxContainerElement = document.getElementsByClassName('boxContainer')[0]; 
  if (boxContainerElement) { 
    var innerBoxElements = boxContainerElement.getElementsByClassName('box'); 
    for (var i = 0; i < innerBoxElements.length; i++) { 
      var boxElement = innerBoxElements[i]; 
      boxElement.innerHTML +='<br /><br /><i>In case we need simple things</i>.'; 
      boxElement.parentNode.className += ' boxsizer'; 
    } 
    var clearFloatDiv = document.createElement('div'); 
    clearFloatDiv.className = 'clear'; 
    boxContainerElement.appendChild(clearFloatDiv); 
  } 
}, 700);

再次使用setTimeout与匿名函数,并将700毫秒设置为第二个参数。在函数内部,我们使用document.getElementById来检索已知在页面中具有唯一 ID 的元素,后来在需要检索具有特定类的所有元素时使用document.getElementsByClassName。我们还使用boxContainerElement.getElementsByClassName('box')来检索所有具有box类的元素,这些元素是具有boxContainer类的元素的后代。

最明显的观察是,在这种情况下,我们需要 18 行代码才能实现相同的结果。相比之下,当使用 jQuery 时,我们只需要 9 行代码,这是后面实现所需行数的一半。使用 jQuery 的$()函数与 CSS 选择器是检索所需元素的更简单的方法,它还确保与不支持getElementsByClassName()方法的浏览器的兼容性。然而,除了代码行数和改进的可读性之外,还有更多的好处。作为复合模式的实施者,$()函数始终检索元素集合,使我们的代码在与我们使用的每个getElement*方法的差异化处理相比更加统一。我们以完全相同的方式使用$()函数,无论我们是只想检索具有唯一 ID 的元素,还是具有特定类的一些元素。

作为返回类似数组的对象的额外好处,jQuery 还可以提供更方便的方法来遍历和操作 DOM,例如我们在第一个示例中看到的.css().append().parent()方法,它们作为返回对象的属性可访问。此外,jQuery 还提供了抽象更复杂的用例的方法,例如没有等效方法可用作 DOM API 的一部分的.addClass().wrap()

由于返回的 jQuery 集合对象除了封装的元素不同之外,我们可以以相同的方式使用 jQuery API 的任何方法。正如我们前面所看到的,这些方法适用于检索到的每个元素,而不管元素计数如何。因此,我们不需要单独的for循环来迭代每个检索到的元素并分别应用我们的操作;相反,我们直接将我们的操作(例如.addClass())应用到集合对象上。

为了在后面的示例中继续提供相同的执行安全保证,我们还需要添加一些额外的if语句来检查null值。这是必需的,因为,例如,如果未找到headerElement,将会发生错误,并且其余的代码行将永远不会被执行。有人可能会认为这些检查,如if (headerElement)if (boxContainerElement)在本示例中不是必需的,可以省略。在这个示例中,这似乎是正确的,但实际上这是在开发大型应用程序时发生错误的主要原因之一,其中元素不断地被创建、插入和删除到 DOM 树中。不幸的是,所有语言和目标平台的程序员都倾向于首先编写他们的实现逻辑,然后在以后的某个时候填写这些检查,通常是在测试实现时出现错误后。

遵循复合模式,即使是一个空的 jQuery 集合对象(不包含任何检索到的元素),仍然是一个有效的集合对象,我们可以安全地应用 jQuery 提供的任何方法。因此,我们不需要额外的if语句来检查集合是否实际包含任何元素,然后应用诸如.css()之类的方法,仅仅是为了避免 JavaScript 运行时错误。

总的来说,jQuery 使用复合模式提供的抽象使得代码行数减少,更易读、统一,并且有更少的易出错行(比较输入$('#elementID')document.getElementById('elementID'))。

使用复合模式开发应用程序

现在我们已经看到了 jQuery 如何在其架构中使用复合模式,并且还进行了比较以及提供的好处,让我们尝试编写一个自己的示例用例。我们将尝试涵盖本章中早期看到的所有概念。我们将结构化我们的复合对象以成为一个类似数组的对象,操作完全不同结构的对象,提供流畅的 API 以允许链式调用,并且具有应用于集合中所有项目的方法。

一个示例用例

假设我们有一个应用程序,某个时刻需要对数字执行操作。另一方面,它需要操作的项目来自不同的来源,且完全不统一。为了使这个示例有趣,假设数据的一个来源提供普通数字,另一个提供具有包含我们感兴趣数字的特定属性的对象:

var numberValues = [2, 5, 8]; 

var objectsWithValues = [ 
    { value: 7 }, 
    { value: 4 }, 
    { value: 6 }, 
    { value: 9 } 
];

在我们使用情景的第二个来源返回的对象可能具有更复杂的结构,可能还有一些额外的属性。这些更改不会以任何方式区分我们的示例实现,因为在开发复合对象时,我们只对提供对目标项目的共同部分进行统一处理感兴趣。

复合集合实现

让我们继续并定义构造函数和原型,来描述我们的组合集合对象:

function ValuesComposite() { 
    this.length = 0; 
} 

ValuesComposite.prototype.append = function(item) { 
    if ((typeof item === 'object' && 'value' in item) || 
        typeof item === 'number') { 
        this[this.length] = item; 
        this.length++; 
    } 

    return this; 
}; 

ValuesComposite.prototype.increment = function(number) { 
    for (var i = 0; i < this.length; i++) { 
        var item = this[i]; 
        if (typeof item === 'object' && 'value' in item) { 
            item.value += number; 
        } else if (typeof item === 'number') { 
            this[i] += number; 
        } 
    } 

    return this; 
}; 

ValuesComposite.prototype.getValues = function() { 
    var result = []; 
    for (var i = 0; i < this.length; i++) { 
        var item = this[i]; 
        if (typeof item === 'object' && 'value' in item) { 
            result.push(item.value); 
        } else if (typeof item === 'number') { 
            result.push(item); 
        } 
    } 
    return result; 
};

在我们的例子中,ValuesComposite() 构造函数非常简单。当使用 new 操作符调用时,它返回一个长度为零的空对象,表示它包装的集合是空的。

注意

有关 JavaScript 基于原型的编程模型的更多信息,请访问 developer.mozilla.org/zh-CN/docs/Web/JavaScript/Introduction_to_Object-Oriented_JavaScript

我们首先需要定义一种方法,使我们能够填充我们的组合集合对象。我们定义了 append 方法,该方法检查提供的参数是否是它可以处理的类型之一;在这种情况下,它将参数附加到组合对象上的下一个可用数字属性,并增加 length 属性值。例如,第一个附加的项,无论是具有值属性的对象还是纯数字,都将暴露给组合对象的 "0" 属性,并可以使用括号属性访问者的语法访问为 myValuesComposition[0]

increment 方法被呈现为一个简单的例子方法,可以通过操作所有集合项来操作这些集合。它接受一个数字值作为参数,然后根据它们的类型适当地处理它,将它添加到我们集合的每个项中。由于我们的组合是类似于数组的对象,increment 使用 for 循环来迭代所有集合项,并增加 item.value(如果项是对象)或存储的实际数字值(当集合项存储的是数字时)。同样地,我们可以继续实现其他方法,例如使我们能够将集合项与特定数字相乘。

为了允许链接我们的组合对象的方法,原型的所有方法都需要返回对对象实例的引用。我们通过简单地在操纵集合的所有方法的最后一行添加 return this; 语句来实现这个目标,例如 appendincrement。请记住,例如 getValues 这样不操纵集合但用于返回结果的方法,根据定义,不能链接到传递集合对象实例的后续方法调用。

最后,我们实现 getValues 方法作为检索我们集合中所有项的实际数字值的便捷方式。与 increment 方法类似,getValues 方法抽象了我们集合的不同项类型之间的处理。它遍历集合项,提取每个数字值,并将它们附加到一个 result 数组中,然后返回给它的调用者。

一个例子执行

现在让我们看一个实际的例子,将使用我们刚刚实现的组合对象:

var valuesComposition = new ValuesComposite(); 

for (var i = 0; i < numberValues.length; i++) { 
    valuesComposition.append(numberValues[i]); 
} 

for (var i = 0; i < objectsWithValues.length; i++) { 
    valuesComposition.append(objectsWithValues[i]); 
}

valuesComposition.increment(2) 
    .append(1) 
    .append(2) 
    .append({ value: 3 }); 

console.log(valuesComposition.getValues()); 

当在浏览器中执行上述代码时,通过将代码编写到现有页面或直接编写到浏览器控制台中,将记录如下结果:

► Array [ 4, 7, 10, 9, 6, 8, 11, 1, 2, 3 ]

我们正在使用我们的数据源,例如前面显示的numberValuesobjectsWithValues变量。上述代码遍历它们并将它们的项附加到一个新创建的组合对象实例上。然后,我们通过 2 递增我们的复合集合的值。紧接着,我们使用append链式三个项目插入,前两个追加数值,第三个追加一个具有值属性的对象。最后,我们使用getValues方法获取一个包含我们集合所有数值的数组,并在浏览器控制台中记录它。

可选的实现方式

请记住,组合对象不一定要是类似数组的对象,但通常偏好于这样的实现,因为 JavaScript 让创建这样的实现变得很容易。另外,类似数组的实现还有一个好处,就是允许我们使用简单的for循环迭代集合项。

另一方面,如果不喜欢类似数组的对象,我们可以轻松地在组合对象上使用一个属性来保存我们的集合项。例如,这个属性可以命名为items,并且可以在我们的方法中使用this.items.push(item)this.items[i]来存储和访问集合的项,分别。

迭代器模式

迭代器模式的关键概念是使用一个负责遍历集合并提供对其项访问的函数。这个函数被称为迭代器,提供了一种访问集合项的方式,而不暴露具体实现和集合对象所使用的底层数据结构。

迭代器提供了关于迭代发生方式的封装级别,将集合项的迭代与其消费者的实现逻辑解耦。

注意

关于单一职责原则的更多信息,请访问www.oodesign.com/single-responsibility-principle.html

jQuery 如何使用迭代器模式

正如我们在本章前面看到的,jQuery 核心$()函数返回一个类似数组的对象,包装了一组页面元素,并提供了一个迭代函数来遍历它并单独访问每个元素。它实际上进一步提供了一个通用的辅助方法jQuery.each(),可以迭代数组、类似数组的对象,以及对象属性。

更多技术描述可以在 jQuery API 文档页面api.jquery.com/jQuery.each/中找到,其中jQuery.each()的描述如下:

一个通用的迭代器函数,可以无缝地迭代对象和数组。数组和具有长度属性的类数组对象(例如函数的参数对象)通过数值索引迭代,从 0 到 length-1。其他对象通过它们的命名属性进行迭代。

jQuery.each()辅助函数在 jQuery 源代码的几个地方内部使用。其中一个用途是遍历 jQuery 对象的条目,并对每个条目应用操作,这正如组合模式所建议的那样。简单搜索关键字.each(会发现有 56 个匹配结果。

注意

在撰写本书时,最新的稳定版本是 v2.2.0,它被用于上述统计信息。

我们可以轻松地跟踪它在 jQuery 源码中的实现,可以通过搜索"each:"(注意有两个出现)或使用 jQuery 源码查看器搜索"jQuery.each()"(就像我们在本章早些时候做的那样):

each: function( obj, callback ) {
  var length, i = 0;

  if ( isArrayLike( obj ) ) {
    length = obj.length;
    for ( ; i < length; i++ ) {
      if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) {
        break;
      }
    }
  } else {
    for ( i in obj ) {
      if ( callback.call( obj[ i ], i, obj[ i ] ) === false ) {
        break;
      }
    }
   }

  return obj;
}

这个辅助函数也可以通过使用之前看到的像.append()这样的方法一样的原型继承在任何 jQuery 对象上访问。你可以轻松找到确切实现这个功能的代码,只需在 jQuery 源码查看器中搜索"jQuery.fn.each()"或者直接搜索 jQuery 源代码中的each:(注意有两个出现的地方):

each: function( callback ) {
  return jQuery.each( this, callback );
}

使用".each()"的方法版本可以让我们以更方便的语法直接迭代 jQuery 集合对象的元素。

下面的示例代码展示了如何在我们的代码中使用两种.each()的方式:

// using the helper function on an array
$.each([3, 5, 7], function(index){
    console.log(this + 1);
});
// using the method on a jQuery object
$('.boxContainer .box').each(function(index) {
    console.log('I\'m box #' + (index + 1)); // index is zero-based
});

当执行时,前面的代码将在浏览器控制台上记录以下内容:

jQuery 如何使用迭代器模式

与组合模式搭配使用

因为组合模式将一个项目集合封装为单个对象,并且迭代器模式可以用于迭代抽象数据结构,所以我们可以很容易地将这两种模式描述为互补的。

可以在哪里使用

迭代器模式可以用于我们的应用程序中抽象化我们从数据结构中访问项目的方式。例如,假设我们需要从以下树形结构中检索大于 4 的所有项目:

var collection = { 
    nodeValue: 7, 
    left: { 
        nodeValue: 4, 
        left: 2, 
        right: { 
            nodeValue: 6, 
            left: 5, 
            right: 9 
        } 
    }, 
    right: { 
        nodeValue: 9, 
        left: 8 
    } 
}; 

现在让我们实现迭代器函数。因为树形数据结构可以有嵌套,所以我们最终得到下面的递归实现:

function iterateTreeValues(node, callback) { 
    if (node === null || node === undefined) { 
        return; 
    } 

    if (typeof node === 'object') { 
        if ('left' in node) { 
            iterateTreeValues(node.left, callback); 
        } 
        if ('nodeValue' in node) { 
            callback(node.nodeValue); 
        } 
        if ('right' in node) { 
            iterateTreeValues(node.right, callback); 
        } 
    } else { 
        // its a leaf, so the node is the value 
        callback(node); 
    } 
} 

最后,我们得到的实现如下所示:

var valuesArray = []; 
iterateTreeValues(collection, function(value) { 
    if (value > 4) { 
        valuesArray.push(value); 
    } 
}); 
console.log(valuesArray);

当执行时,前面的代码将在浏览器控制台上记录以下内容:

► Array [ 5, 6, 9, 7, 8, 9 ]

我们可以清楚地看到迭代器简化了我们的代码。我们再也不需要每次访问满足特定条件的一些项目时烦恼于使用的数据结构的实现细节。我们的实现建立在迭代器公开的通用 API 之上,并且我们的实现逻辑出现在我们为迭代器提供的回调中。

这种封装使我们能够将我们的实现与所使用的数据结构解耦,前提是将提供具有相同 API 的迭代器。例如,在这个例子中,我们可以轻松地将使用的数据结构更改为排序的二叉树或简单数组,并保持我们的实现逻辑不变。

摘要

在本章中,我们对 JavaScript 的 DOM 脚本 API 和 jQuery 进行了复习。我们介绍了复合模式,并看到了它是如何被 jQuery 库使用的。我们看到了复合模式如何简化我们的工作流程,当我们在不使用 jQuery 的情况下重新编写了我们的示例页面之后,并且随后展示了在我们的应用程序中使用复合模式的示例。最后,我们介绍了迭代器模式,并看到了当与复合模式一起使用时它是多么出色。

现在我们已经完成了关于复合模式在我们日常使用 jQuery 方法中发挥重要作用的介绍,我们可以继续下一章,在那里我们将展示观察者模式以及使用 jQuery 在我们的页面中方便地利用它的方式。

第二章:观察者模式

在本章中,我们将展示观察者模式以及我们如何使用 jQuery 在我们的页面中方便地利用它。随后,我们还将解释委托事件观察者模式的变体,当正确应用于网页时,可以简化代码并减少页面所需的内存消耗。

在本章中,我们将:

  • 介绍观察者模式

  • 查看 jQuery 如何使用观察者模式

  • 将观察者模式与使用事件属性进行比较

  • 学习如何避免观察者引起的内存泄漏

  • 介绍委托事件观察者模式并展示其好处

介绍观察者模式

观察者模式的关键概念是有一个对象,通常称为可观察对象或主体,在其生命周期内其内部状态会发生变化。还有其他几个对象,被称为观察者,它们希望在可观察对象/主体的状态发生变化时被通知,以执行一些操作。

观察者可能需要被通知关于可观察对象的任何状态改变,或者仅特定类型的改变。在最常见的实现中,可观察对象维护一个观察者列表,并在适当的状态改变发生时通知它们。如果可观察对象发生状态改变,它会遍历对那种类型的状态改变感兴趣的观察者列表,并执行它们定义的特定方法。

介绍观察者模式

根据观察者模式的定义和计算机科学书籍中的参考实现,观察者被描述为实现了众所周知的编程接口的对象,大多数情况下,该接口对于它们感兴趣的每个可观察对象都是特定的。在状态改变的情况下,可观察对象将执行每个观察者的众所周知方法,因为它在编程接口中被定义。

注意

有关在传统的面向对象编程中如何使用观察者模式的更多信息,您可以访问 www.oodesign.com/observer-pattern.html

在 Web 堆栈中,观察者模式通常使用普通的匿名回调函数作为观察者,而不是具有众所周知方法的对象。可以通过等效结果实现观察者模式,因为回调函数保留了对其定义所在环境的变量的引用——这种模式通常被称为闭包。使用观察者模式而不是回调作为调用或初始化参数的主要优点是观察者模式可以支持单个目标上的几个独立处理程序。

注意

有关闭包的更多信息,您可以访问 developer.mozilla.org/en-US/docs/Web/JavaScript/Closures

提示

定义简单回调

回调可以定义为作为另一个函数/方法的参数传递或分配给对象的属性,并且期望在稍后的某个时间点执行的函数。通过这种方式,将我们的回调交给的代码将调用它,将操作或事件的结果传播回定义回调的上下文。

由于将函数注册为观察者的模式被证明更灵活和更简单直接的编程,它在网页堆栈之外的编程语言中也可以找到。其他编程语言通过语言特性或特殊对象(如子例程、lambda 表达式、块和函数指针)提供了等效的功能。例如,Python 也像 JavaScript 一样将函数定义为一等对象,使它们能够被用作回调函数,而 C#则定义了委托作为特殊对象类型,以实现相同的结果。

观察者模式是开发响应用户操作的 Web 界面的一个组成部分,每个 Web 开发人员都在某种程度上使用它,即使在不知情的情况下也是如此。这是因为创建丰富用户界面时,Web 开发人员需要做的第一件事是向页面元素添加事件侦听器,并定义浏览器应该如何响应它们。

传统上,这是通过在需要监听事件的页面元素上使用EventTarget.addEventListener()方法实现的,例如“点击”,并提供一个回调函数,其中包含需要在事件发生时执行的代码。值得一提的是,为了支持旧版本的 Internet Explorer,需要测试EventTarget.attachEvent()的存在,并使用它来代替。

注意

有关addEventListener()attachEvent()方法的更多信息,您可以访问developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListenerdeveloper.mozilla.org/en-US/docs/Web/API/EventTarget/attachEvent

jQuery 如何使用它

jQuery 库在其实现的几个部分中大量使用观察者模式,直接使用addEventListener方法或创建其自己的抽象来实现。此外,jQuery 提供了一系列抽象和方便的方法,使在 Web 上使用观察者模式变得更容易,并且还在内部使用其中一些方法来实现其他方法。

jQuery 的 on 方法

jQuery.fn.on()方法是将事件处理程序附加到元素的中央 jQuery 方法,提供了一种简单的方法来采用观察者模式,同时保持我们的代码易于阅读和理解。它将所请求的事件处理程序附加到由$()函数返回的复合 jQuery 集合对象的所有元素上。

在 jQuery 源码查看器中搜索 jQuery.fn.on(可在 james.padolsey.com/jquery 找到),或直接在 jQuery 源代码中搜索 on: function(第一个字符是制表符),将引导我们找到方法的定义,代码共有 67 行。事实上,在内部 on 函数的前 55 行只是处理 jQuery.fn.on() 方法可以被调用的不同方式;接近末尾,我们能看到它实际上使用了内部方法 jQuery.event.add()

jQuery.fn.extend({
  on: function( types, selector, data, fn ) {
    return on( this, types, selector, data, fn );
  }
});

function on( elem, types, selector, data, fn, one ) {

  /* 55 lines of code handling the method overloads */
  return elem.each( function() {
    jQuery.event.add( this, types, fn, data, selector );
  } );
}
 trimmed down version of that method, where some code related to the technical implementation of jQuery and not related to the Observer Pattern has been removed for clarity:
add: function( elem, types, handler, data, selector ) { 
    /* ... 4 lines of code ... */
        elemData = dataPriv.get( elem ); 
    /* ... 13 lines of code ... */

    // Make sure that the handler has a unique ID, 
    // used to find/remove it later 
 if ( !handler.guid ) { 
 handler.guid = jQuery.guid++; 
 } 

    // Init the element's event structure and main handler, 
    // if this is the first 
 if ( !( events = elemData.events ) ) { 
 events = elemData.events = {}; 
 } 
    /* ... 9 lines of code ... */ 

    // Handle multiple events separated by a space 
    types = ( types || "" ).match( rnotwhite ) || [ "" ]; 
    t = types.length; 
    while ( t-- ) { 
        /* ... 30 lines of code ... */ 

        // Init the event handler queue if we're the first 
        if ( !( handlers = events[ type ] ) ) { 
 handlers = events[ type ] = []; 
            handlers.delegateCount = 0; 

            // Only use addEventListener if the special events handler
            // returns false 
            if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) {
 if ( elem.addEventListener ) { 
 elem.addEventListener( type, eventHandle ); 
 } 
            } 
        }

        /* ... 9 lines of code ... */ 

        // Add to the element's handler list, delegates in front 
 if ( selector ) { 
 handlers.splice( handlers.delegateCount++, 0, handleObj ); 
 } else { 
 handlers.push( handleObj ); 
 }
        /* ... 3 lines of code ... */
    } 
}

现在,让我们通过引用前面高亮的代码,了解 jQuery.event.add() 如何实现观察者模式。

jQuery.event.add() 方法的参数中的 handler 变量存储最初作为参数传递给 jQuery.fn.on() 方法的函数。我们可以称这个函数为我们的观察器函数,因为它在附加到的元素上触发相应事件时被执行。

在第一个高亮的代码区域中,jQuery 创建并给存储在 handler 变量中的观察器函数分配了一个 guid 属性。记住,在 JavaScript 中,可以给函数赋值属性,因为函数是一流对象。jQuery.guid++ 语句在分配旧值之后执行,这是因为 jQuery.guid 是 jQuery 和 jQuery 插件在内部使用的全局计数器。观察器函数上的 guid 属性用作标识和定位 jQuery 为每个元素维护的观察器函数列表中的观察器函数的一种方式。例如,jQuery.fn.off() 方法使用它来定位并从与元素关联的观察器函数列表中删除观察器函数。

小贴士

jQuery.guid 是一个页面范围的计数器,它被插件和 jQuery 本身用作集中的方式来检索唯一的整数 ID。它通常用于给元素、对象和函数分配唯一的 ID,以便更容易地在集合中定位它们。每个检索和使用 jQuery.guid 当前值的实现者都有责任在每次使用后也增加属性值(加一)。否则,由于这是一个页面范围的计数器,被 jQuery 插件和 jQuery 自己用于标识,页面可能会面临难以调试的故障。

在第二个和第三个突出显示的代码区域中,jQuery 初始化一个数组来保存每个可能在该元素上触发的事件的观察者列表。需要注意的是,第二个突出显示的代码区域中的观察者列表并不是实际 DOM 元素的属性。正如 jQuery.event.add() 方法开头附近的 dataPriv.get( elem ) 语句所示,jQuery 使用单独的映射对象来保存 DOM 元素与其观察者列表之间的关联。通过使用这种数据缓存机制,jQuery 能够避免向 DOM 元素添加额外属性,这些属性是其实现所需要的。

注意

您可以通过搜索 function Data() 在 jQuery 源代码中轻松找到数据缓存机制的实现。这将带您到 Data 类的构造函数,该构造函数后面跟随着在 Data.prototype 对象中定义的类方法的实现。有关更多信息,您可以访问 api.jquery.com/data

下一个突出显示的代码区域是 jQuery 检查 EventTarget.addEventListener() 方法是否实际上对该元素可用,然后使用它将事件监听器添加到该元素。在最后一个突出显示的代码区域中,jQuery 将观察者函数添加到其内部列表中,该列表保存了附加到该特定元素的相同事件类型的所有观察者。

注意

根据您所使用的版本,可能会在某种程度上获得不同的结果。编写本书时发布和使用的最新稳定的 jQuery 版本是 v2.2.0。

如果您需要为旧版浏览器(例如低于版本 9 的 Internet Explorer)提供支持,则应使用 jQuery 的 v1.x 版本。编写本书时的最新版本是 v1.12.0,它提供与 v2.2.x 版本完全相同的 API,但也具有在旧版浏览器上运行所需的代码。

为了涵盖旧版浏览器的实现不一致性,jQuery v1.x 中 jQuery.event.add() 的实现要长一些,更复杂一些。其中一个原因是因为 jQuery 还需要测试浏览器是否实际上支持 EventTarget.addEventListener(),如果不是,则尝试使用 EventTarget.attachEvent()

正如我们在前面的代码中看到的,jQuery 的实现遵循观察者模式描述的操作模型,但也融入了一些实现技巧,以使其与 Web 浏览器可用的 API 更有效地配合工作。

文档准备就绪的观察者

jQuery 提供的另一个方便的方法,是被开发人员广泛使用的$.fn.ready()方法。此方法接受一个函数参数,仅在页面的 DOM 树完全加载后才执行它。这在以下情况下可能会有用:如果您的代码不是最后加载到页面上,而且您不想阻塞初始页面呈现,或者它需要操作的元素被定义在其自身<script>标签之后。

注意

请记住,$.fn.ready()方法的工作方式与window.onload回调和页面的"load"事件稍有不同,它们会等待页面的所有资源加载完毕。有关更多信息,您可以访问api.jquery.com/ready

以下代码演示了$.fn.ready()方法的最常见使用方式:

$(document).ready(function() {
    /* this code will execute only after the page has been fully loaded */ 
})

如果我们尝试找到jQuery.fn.ready的实现,我们会看到它实际上在内部使用jQuery.ready.promise来工作:

jQuery.fn.ready = function( fn ) { 
  // Add the callback 
  jQuery.ready.promise().done( fn ); 

  return this; 
};
/* … a lot lines of code in between */
jQuery.ready.promise = function( obj ) { 
  if ( !readyList ) { 

    readyList = jQuery.Deferred(); 

    // Catch cases where $(document).ready() is called
    // after the browser event has already occurred.
    // Support: IE9-10 only
    // Older IE sometimes signals "interactive" too soon
    if ( document.readyState === "complete" || ( document.readyState !== "loading" && !document.documentElement.doScroll ) ) {
      // Handle it asynchronously to allow ... to delay ready 
      window.setTimeout( jQuery.ready ); 

    } else { 
      // Use the handy event callback 
 document.addEventListener( "DOMContentLoaded", completed ); 

      // A fallback to window.onload, that will always work 
 window.addEventListener( "load", completed ); 
    } 
  } 
  return readyList.promise( obj ); 
};

正如您在实现中前面高亮显示的代码区域中所见,jQuery 使用addEventListener来观察document对象上的DOMContentLoaded事件何时触发。另外,为了确保它在各种浏览器中都能工作,它还注意到window对象上的load事件何时被触发。

jQuery 库还提供了在代码中添加上述功能的更短的方法。由于上述实现实际上不需要对文档的引用,因此我们可以用$().ready(function() {/* ... */ })来代替。还存在一个$()函数的重载,它能够达到相同的效果,它的使用方式是$(function() {/* ... */ })。这两种替代方法使用jQuery.fn.ready在开发者中受到了严重批评,因为它们通常会导致误解。尤其是第二种更为简短的版本会引起混淆,因为它看起来像一个立即调用的函数表达式IIFE),这是 JavaScript 开发人员大量使用和已学会识别的一种模式。实际上,它只有一个字符($)的不同,因此在与开发团队讨论之前不建议使用它。

注意

$.fn.ready()方法也被称为为我们的代码实现惰性初始化/执行模式提供了一种简单的方法。该模式的核心概念是推迟执行一段代码或在以后的时间点加载远程资源。例如,我们可以等待页面完全加载后再添加观察者,或者在下载网页资源之前等待某个特定事件发生。

演示一个样本用例

为了看到观察者模式的实际效果,我们将创建一个示例来展示控制面板的骨架实现。在我们的示例中,用户将能够向其控制面板添加与标题栏中可供选择的一些示例项目和类别相关的信息框。

我们的示例将为我们的项目设有三个预定义的类别:产品销售广告。每个类别都将有一系列相关项目,这些项目将出现在类别选择器正下方的区域。用户可以通过使用下拉选择器选择所需的类别,这将更改仪表板的可见选择项目。

演示一个使用案例

我们的仪表板最初将包含关于仪表板用法的提示信息框。每当用户点击类别项之一时,一个新的信息框将出现在我们的三列布局仪表板中。在前述图像中,用户通过点击相关按钮为产品 B产品 D添加了两个新的信息框。

演示一个使用案例

用户还可以通过点击每个信息框顶部右侧的红色关闭按钮来取消这些信息框中的任何一个。在前述图像中,用户取消了产品 D信息框,然后添加了广告 3以及销售类别的第 1、2 和 3 周项目的信息框。

通过仅仅阅读上述描述,我们可以轻松地分离出所有实现我们仪表板所需的用户交互。我们将需要为每一个这些用户交互添加观察者,并在回调函数中编写执行适当 DOM 操作的代码。

具体来说,我们的代码将需要:

  • 观察当前选定元素所做的更改,并通过隐藏或显示相应项目来响应此类事件

  • 观察每个项目按钮的点击并通过添加新的信息框来响应

  • 观察每个信息框的关闭按钮的点击并通过将其从页面中移除来响应

现在让我们继续并查看所需的 HTML、CSS 和 JavaScript 代码,以完成前面的示例。让我们从 HTML 代码开始,假设我们将其保存在名为Dashboard Example.html的文件中,代码如下:

<!DOCTYPE html> 
<html> 
  <head> 
    <title>Dashboard Example</title> 
    <link rel="stylesheet" type="text/css" href="dashboard-example.css"> 
  </head> 
  <body> 
    <h1 id="pageHeader">Dashboard Example</h1> 

    <div class="dashboardContainer"> 
      <section class="dashboardCategories"> 
        <select id="categoriesSelector"> 
          <option value="0" selected>Products</option> 
          <option value="1">Sales</option> 
          <option value="2">Advertisements</option> 
        </select> 
        <section class="dashboardCategory"> 
          <button>Product A</button> 
          <button>Product B</button> 
          <button>Product C</button> 
          <button>Product D</button> 
          <button>Product E</button> 
        </section> 
        <section class="dashboardCategory hidden"> 
          <button>1st week</button> 
          <button>2nd week</button> 
          <button>3rd week</button> 
          <button>4th week</button> 
        </section> 
        <section class="dashboardCategory hidden"> 
          <button>Advertisement 1</button> 
          <button>Advertisement 2</button> 
          <button>Advertisement 3</button> 
        </section> 
        <div class="clear"></div> 
      </section> 

      <section class="boxContainer"> 
        <div class="boxsizer"> 
          <article class="box"> 
            <header class="boxHeader"> 
              Hint! 
              <button class="boxCloseButton">&#10006;</button> 
            </header> 
            Press the buttons above to add information boxes... 
          </article> 
        </div> 
      </section> 
      <div class="clear"></div> 
    </div> 

    <script type="text/javascript" src="img/jquery.js"></script> 
    <script type="text/javascript" src="img/dashboard-example.js">
    </script> 
  </body> 
</html>

在前述 HTML 中,我们将所有与仪表板相关的元素放在带有dashboardContainer CSS 类的<div>元素内。这将使我们能够有一个中心起点来搜索我们仪表板的元素,并且作用域我们的 CSS。在它内部,我们使用一些 HTML5 语义元素定义了两个<section>元素,以便使用逻辑区域划分仪表板。

第一个带有dashboardCategories类的<section>用于保存我们仪表板的类别选择器。在其中,我们有一个带有 ID categoriesSelector<select>元素,用于过滤可见的类别项目,以及三个带有dashboardCategory类的子部分,用于包装在单击时将用信息框填充仪表板的<button>元素。其中两个还具有hidden类,以便在页面加载时仅显示第一个,通过匹配类别选择器的最初选择选项(<option>)。此外,在第一节的末尾,我们还添加了一个带有clear类的<div>,正如我们在第一章中看到的那样,它将用于清除浮动的<button>元素。

带有boxContainer类的第二个<section>用于保存我们仪表板的信息框。最初,它仅包含一个关于如何使用仪表板的提示。我们使用带有boxsizer类的<div>元素来设置框尺寸,以及带有box类的 HTML5 <article> 元素来添加所需的边框填充和阴影,类似于第一章中的框元素。

每个信息框除了其内容之外,还包含一个带有boxHeader类的<header>元素和一个带有boxCloseButton类的<button>元素,当点击时,会移除包含它的信息框。我们还使用了&#10006; HTML 字符代码作为按钮的内容,以获得更漂亮的“x”标记,并避免使用单独的图像来实现此目的。

最后,由于信息框也是浮动的,我们还需要一个带有clear类的<div>放置在boxContainer的末尾。

在前述 HTML 的<head>中,我们还引用了一个名为dashboard-example.css的 CSS 文件,其内容如下:

.dashboardCategories { 
    margin-bottom: 10px; 
} 

.dashboardCategories select, 
.dashboardCategories button { 
    display: block; 
    width: 200px; 
    padding: 5px 3px; 
    border: 1px solid #333; 
    margin: 3px 5px; 
    border-radius: 3px; 
    background-color: #FFF; 
    text-align: center; 
    box-shadow: 0 1px 1px #777; 
    cursor: pointer; 
} 

.dashboardCategories select:hover, 
.dashboardCategories button:hover { 
    background-color: #DDD; 
} 

.dashboardCategories button { 
    float: left; 
} 

.box { 
    padding: 7px 10px; 
    border: solid 1px #333; 
    margin: 5px 3px; 
    box-shadow: 0 1px 2px #777; 
} 

.boxsizer { 
    float: left; 
    width: 33.33%; 
} 

.boxHeader { 
    padding: 3px 10px;
    margin: -7px -10px 7px;
    background-color: #AAA; 
    box-shadow: 0 1px 1px #999; 
} 

.boxCloseButton { 
    float: right; 
    height: 20px; 
    width: 20px; 
    padding: 0; 
    border: 1px solid #000; 
    border-radius: 3px; 
    background-color: red; 
    font-weight: bold; 
    text-align: center; 
    color: #FFF; 
    cursor: pointer; 
} 

.clear { clear: both; } 
.hidden { display: none; }

正如您在我们的 CSS 文件中所看到的,首先我们在具有dashboardCategories类的元素下面添加了一些空间,并且为<select>元素和其中的按钮定义了相同的样式。为了使其与默认浏览器样式区分开来,我们添加了一些填充,圆角边框,悬停鼠标指针时的不同背景颜色以及它们之间的一些空间。我们还定义了我们的<select>元素应该作为块独自显示在其行中,以及分类项目按钮应该相邻浮动。我们再次使用了boxsizerbox CSS 类,就像在第一章,jQuery 和组合模式复习中所做的一样;第一个用于创建三列布局,第二个实际提供信息框的样式。我们继续定义boxHeader类,应用于我们信息框的<header>元素,并定义一些填充,灰色背景颜色,轻微阴影,以及一些负边距,以抵消框填充的效果并将其放置在其边框旁边。

要完成信息框的样式设计,我们还定义了boxCloseButton CSS 类,它(i)将框的关闭按钮浮动到框的<header>内的右上角,(ii)定义了20px的宽度和高度,(iii)覆盖了默认浏览器的<button>样式以零填充,并且(iv)添加了一个单像素的黑色边框,圆角和红色背景颜色。最后,就像在第一章,jQuery 和组合模式复习中,我们定义了clear实用的 CSS 类以防止元素被放置在前面浮动元素的旁边,并且还定义了hidden类作为隐藏页面元素的方便方式。

在我们的 HTML 文件中,我们引用了 jQuery 库本身以及一个名为dashboard-example.js的 JavaScript 文件,其中包含我们的仪表板实现。遵循创建高性能网页的最佳实践,我们将它们放在了</body>标签之前,以避免延迟初始页面渲染:

$(document).ready(function() { 

    $('#categoriesSelector').change(function() { 
        var $selector = $(this); 
        var selectedIndex = +$selector.val(); 
        var $dashboardCategories = $('.dashboardCategory'); 
        var $selectedItem = $dashboardCategories.eq(selectedIndex).show(); 
        $dashboardCategories.not($selectedItem).hide();
    }); 

    function setupBoxCloseButton($box) { 
        $box.find('.boxCloseButton').click(function() { 
            $(this).closest('.boxsizer').remove(); 
        }); 
    } 

    // make the close button of the hint box work 
    setupBoxCloseButton($('.box')); 

    $('.dashboardCategory button').on('click', function() { 
        var $button = $(this); 
        var boxHtml = '<div class="boxsizer"><article class="box">' + 
                '<header class="boxHeader">' + 
                    $button.text() + 
                    '<button class="boxCloseButton">&#10006;' + 
                    '</button>' + 
                '</header>' + 
                'Information box regarding ' + $button.text() + 
            '</article></div>'; 
        $('.boxContainer').append(boxHtml); 
        setupBoxCloseButton($('.box:last-child')); 
    });

}); 

我们将所有代码放在了一个$(document).ready()调用中,以延迟其执行直到页面的 DOM 树完全加载。如果我们将代码放在<head>元素中,这将是绝对必要的,但在任何情况下遵循的最佳实践也是很好的。

首先,我们使用$.fn.change()方法为categoriesSelector元素的change事件添加了一个观察者。实际上,这是$.fn.on('change', /* … */)``方法的一种简写方法。在 jQuery 中,作为观察者使用的函数内的this关键字的值保存着被触发事件的 DOM 元素的引用。这适用于所有注册观察者的 jQuery 方法,从核心的\(.fn.on()`到方便的`\).fn.change()\(.fn.click()`方法。所以我们使用`\)()函数用

posted @ 2024-05-19 20:13  绝不原创的飞龙  阅读(12)  评论(0编辑  收藏  举报