65.9K
CodeProject 正在变化。 阅读更多。
Home

使用指令 iv-on-cmd 在 Angular 中减少事件和绑定

2015年1月20日

MIT

5分钟阅读

viewsIcon

6657

指令 iv-on-cmd 可用于简化 Angular 点击事件的处理。

引言

iv-on-cmd 是一个 Angular 指令,它允许单个事件处理程序处理包含指令 iv-on-cmd 的元素的**所有子元素**的点击事件,前提是这些子元素设置了 data-cmd 属性。

iv-on-cmd 的目标是减少 ng-click 处理程序的数量,并提供一个通用的函数来处理所有子元素。

背景

我发现 Angular 在绑定过多时会变慢,并且我读过一些文章表明,即使是两千个绑定也可能太多了。

在我参与的几个项目中,ng-repeat 的结果会创建数百到数千个 DOM 元素。每个元素都有三到三十个绑定。这远远超过了预计的慢速阈值两千。

我需要一种方法来做我以前用 jQuery 就能做到的事情。那就是在一个包装器 <div> 上使用一个单一的点击处理程序,它告诉我需要为包装器的所有子元素执行哪个命令。使用 jQuery,我会这样做:

<div class="main-wrapper">
  <button class="start-process">Start</button>
  <button class="cancel">cancel</button>
</div>

上面的 DOM 极度简化,但对本说明来说足够了。

对于上面的 HTML 示例,我会添加一些 jQuery 代码来独立处理每个 <button>

function startProcess() {
  // Do some kind of process here
}

function cancel() {
  // Cancel the operation here
}

$(document).ready(function() {
  $(".start-process").on("click", startProcessFn);
  $(".cancel").on("click", cancelFn);
});

但是当我有数百个这样的按钮时,或者其他 DOM 元素时,编写每一个 click 处理程序都成了一项巨大的工程。

Angular 通过允许您使用 ng-click 指令设置点击处理程序来改进这一点,如下所示:

<div class="main-wrapper" data-ng-controller="myCtrl">
  <button data-ng-click="startProcess()">Start</button>
  <button data-ng-click="cancel()">cancel</button>
</div>

然后在控制器代码中,我会这样做:

angular.module("mine").controller("myCtrl", myCtrl);
myCtrl.$inject = ["$scope"];
function myCtrl($scope) {
  $scope.startProcess = function() {
    // Do some kind of process here
  };

  $scope.cancel = function() {
    // Cancel the operation here
  }
}

但是,即使有数百个 ng-click 指令,这仍然可能导致大量的绑定。

Using the Code

我的 jQuery 解决方案

为了解决 jQuery 中的大量事件绑定问题,我创建了一个命令处理程序。它会利用 $().on() 函数的 delegate 版本。这是通过在父元素上设置 $().on() 处理程序,并指定将调用您代码的子元素来实现的。所以,对于这个 HTML:

<div class="main-wrapper">
  <button data-cmd="start-process">Start</button>
  <button data-cmd="cancel">cancel</button>
</div>

脚本看起来会像这样:

function processCmd(event) {
  var $el = $(event.target);
  var cmd = $el.data("cmd");

  console.log("Command was %s", cmd); 

  // Process the commands.
  switch(cmd) {
    case "start-process":
      // Do something
      break;

    case "cancel":
      // Do something
      break;
  }
}

$(document).ready(function() {
  $(".main-wrapper").on("click", "[data-cmd]", processCmd);
});

有了这段代码,click 处理程序就是一个委托处理程序。这意味着当用户点击 <button> 时,事件会被委托给连接到 <div> 标签的事件处理程序。但仅限于具有 data-cmd 属性的按钮。现在,即使有数百个按钮,我也只有一个事件处理程序。而且,如果稍后添加按钮,我的事件处理程序仍然会被调用。

上面的例子很小,可能无法充分说明我所描述的内容。但想象一下有某种重复的元素,它们之间唯一的区别是索引值或某种键值。以一个基于 Web 的消息应用程序为例。每条消息都有自己唯一的标识符。如果每条消息都有一个“已读”按钮和一个“删除”按钮,那么每条消息就需要两个事件处理程序。但是,通过使用 $().on() 的委托版本,我们可以有一个事件处理程序来处理所有消息。

<div class="mail-shell">
  <div class="message">
    <span class="sender">someone@example.com</span>
    <span class="subject">Some message subject</span>
    <span class="time">3:43 am</span>
    <span><button data-cmd="read" data-cmd-data="KE1R-DJ5KW-9SJ21">Read</button></span>
    <span><button data-cmd="delete" data-cmd-data="KE1R-DJ5KW-9SJ21">Delete</button></span>
  </div>
  <div class="message">
    <span class="sender">person@example.com</span>
    <span class="subject">Buy something from us</span>
    <span class="time">2:49 am</span>
    <span><button data-cmd="read" data-cmd-data="K19D-0PWR8-MMK92">Read</button></span>
    <span><button data-cmd="delete" data-cmd-data="K19D-0PWR8-MMK92">Delete</button></span>
  </div>
  <div class="message">
    <span class="sender">bot@example.com</span>
    <span class="subject">Buy a Rolex from us</span>
    <span class="time">2:31 am</span>
    <span><button data-cmd="read" data-cmd-data="LK0P-HN8GT-00LPD">Read</button></span>
    <span><button data-cmd="delete" data-cmd-data="LK0P-HN8GT-00LPD">Delete</button></span>
  </div>
</div>

现在想象一下,与上面的例子中的三个消息相比,有成百上千条这样的消息。

使用几行代码和单个事件处理程序,我们可以处理所有按钮的 click 事件,即使在设置了事件处理程序后,新的消息仍然会出现。

function processCmd(event) {
  var $el = $(event.target);
  var cmd = $el.data("cmd");
  var cmdData = $el.data("cmdData");
  switch(cmd) {
    case "read":
      openMessage(cmdData);
      break;

    case "delete":
      deleteMessage(cmdData);
      break;
  }
}

$(document).ready(function() {
  $(".main-wrapper").on("click", "[data-cmd]", processCmd);
});

Angular 指令:iv-on-cmd

我的 Angular 指令 iv-on-cmd 利用了 jQuery 的委托功能来简化和减少所需的 Angular 代码量。它会为您做一些幕后工作。它会找出命令 cmd 是什么以及命令数据 cmdData 是什么,并将它们插入到 $event.data 对象中。然后,它会将 $event 传递给您的处理程序。

下面的 HTML 示例在外部 <div> 上使用了 iv-on-cmd 指令。这使得一个事件处理程序 processCmd() 可以处理来自三个子按钮的所有点击事件。

<div data-ng-controller="myCtrl" data-iv-on-cmd="processCmd($event)">
  <button data-cmd="sayHello">Say Hello</button>
  <button data-cmd="speak" data-cmd-data="Hi">Say Hi</button>
  <button data-cmd="speak" data-cmd-data="Bye">Say Bye</button>
</div>

下面的示例控制器提供了 processCmd() 函数,只要用户点击带有 data-cmd 属性的按钮之一,就会访问该函数。

angular.module("mine").controller("myCtrl", myCtrl);
myCtrl.$inject = ["$scope"];
function myCtrl($scope) {
  $scope.processCmd = function($event) {
    $event.stopPropigation();
    $event.preventDefault();
     if ($event.data.cmd === "sayHello") {
       alert("Hello");
       return;
     }

     if ($event.data.cmd === "speak" ) {
        alert("Speaking: " + $event.data.cmdData);
        return;
     }
  }
}

在具有 data-cmd="speak" 的按钮中,代码还将使用 data-cmd-data 属性。此属性值将被读取并与 data-cmd 的值一起放入 $event.data 对象。

对于此按钮:

<button data-cmd="sayHello">Say Hello</button>

对象 $event.data 将是:

{
  "cmd": "sayHello",
  "cmdData": undefined
}

对于此按钮:

<button data-cmd="speak" data-cmd-data="Hi">Say Hi</button>

对象 $event.data 将是:

{
  "cmd": "speak",
  "cmdData": "Hi"
}

您也可以在 data-cmd-data 属性中包含对象。

对于此按钮:

<button data-cmd="buy" 
data-cmd-data='{"title":"Test Product", "price": 3.95}'>Buy Now</button>

对象 $event.data 将是:

{
  "cmd": "buy",
  "cmdData": {
    "title": "Test Product",
    "price": 3.95
  }
}

示例:菜单和工具栏

我有一个项目,它同时拥有一个菜单(带子菜单)和一个工具栏。大多数菜单项都被工具栏元素复制了。这样,用户就可以通过菜单或通过工具栏按钮执行相同的操作。

这是项目中的菜单和工具栏:

Menu and toolbar

这是带有已打开子菜单的菜单:

Menu and toolbar

菜单是使用一个指令创建的,工具栏是使用第二个指令创建的。但是,这两个指令都只是将 data-cmd 属性添加到 DOM 元素,而没有处理点击事件(除了菜单可以切换子菜单的打开和关闭)。

菜单和工具栏包含在一个单独的 <div> 中,我将 iv-on-cmd 指令添加到这个 <div> 上,如下所示:

<div data-iv-on-cmd="processCmd($event)">
  <ul class="menu">
    <li>
      <button data-ng-click="toggle('batch')">Batch</button>
      <ul class="sub-menu" data-menu="batch">
        <li>...</li>
        ...
      </ul>
    </li>
    <li>
      <button data-ng-click="toggle('image')">Image</button>
      <ul class="sub-menu" data-menu="image">
        <li><button data-cmd="ruler">Ruler</button></li>
        <li><button data-cmd="highlights">Highlights</button></li>
      </ul>
    </li>
    ...
  </ul>
  <div class="toolbar">
    <button class="toolbar__button" 
    data-cmd="unto"><img src="img/undo.png"></button>
    <button class="toolbar__button" 
    data-cmd="redo"><img src="img/redo.png"></button>
    <span class="toolbar__separator"></span>
    <button class="toolbar__button" 
    data-cmd="cut"><img src="img/cut.png"></button>
    <button class="toolbar__button" 
    data-cmd="copy"><img src="img/copy.png"></button>
    <button class="toolbar__button" 
    data-cmd="paste"><img src="img/paste.png"></button>
    <span class="toolbar__separator"></span>
    ...
  </div>
</div>

控制器提供了一个单一的函数 processCmd(),它将处理每个命令。

angular.module("mine").factory("myService", myService);
function myService() {
  return {
    "undo": undoFn,    
    "redo": redoFn,    
    "cut": cutFn,    
    "copy": copyFn,    
    "paste": pasteFn    
  }

  function undoFn() {
    // Do something
  }

  function redoFn() {
    // Do something
  }

  function cutFn() {
    // Do something
  }

  function copyFn() {
    // Do something
  }

  function pasteFn() {
    // Do something
  }
}

angular.module("mine").controller("myCtrl", myCtrl);
myCtrl.$inject = ["$scope", "myService"];
function myCtrl($scope, myService) {
  $scope.processCmd = function($event) {
    $event.stopPropigation();
    $event.preventDefault();
    var cmd = $event.data.cmd;

    if (myService.hasOwnProperty(cmd)) { // See is the service supports the command
      myService[cmd]($event.data.cmdData); // $event.data.cmd will default to undefined
    }
    else {
      // Display an error or throw an exception
      // The cmd is not supported in the service 
    }
  }
}

这是一个非常简单的例子。但它表明,我们可以生成简单的 HTML,提供 data-cmd 属性。然后,通过一个单一的命令处理程序,我们可以处理这些命令。在此示例中,我还将处理命令的工作转移到了一个服务中。尽管您可能需要执行异步操作或从服务调用中获取数据,这会改变代码的编写方式。

关注点

需要 jQuery 而不是 jqLite

此指令要求您在加载 Angular 之前加载 jQuery。它不适用于 Angular 中的 jqLite,因为 jqLite 不支持 $().on() 函数的委托模式。

需要 jQuery 1.7 或更高版本,因为这些版本支持 $().on() 函数的委托模式。

<script src="jquery_min.js"></script>
<script src="angular_min.js"></script>

源代码

源代码可作为 Github 项目的一部分提供:angular-tools 项目

您也可以直接访问iv-on-cmd 指令

© . All rights reserved.