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





0/5 (0投票)
指令 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
}
}
示例:菜单和工具栏
我有一个项目,它同时拥有一个菜单(带子菜单)和一个工具栏。大多数菜单项都被工具栏元素复制了。这样,用户就可以通过菜单或通过工具栏按钮执行相同的操作。
这是项目中的菜单和工具栏:
这是带有已打开子菜单的菜单:
菜单是使用一个指令创建的,工具栏是使用第二个指令创建的。但是,这两个指令都只是将 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 指令。