AngularJS:在指令中绑定到全局事件的最好方法是什么?
想象一下你想创build一个需要响应全局事件的指令的AngularJS中的情况。 在这种情况下,我们说,窗口大小调整事件。
什么是最好的办法呢? 我看到它的方式,我们有两个select:1.让每个指令绑定到事件,并对当前元素做魔术2.创build一个全局事件侦听器,做一个DOMselect器来获取逻辑应该在其上的每个元素应用。
选项1的优点是您已经可以访问要执行某些操作的元素。 但是…选项2的优点是,您不必为同一个事件绑定多次(对于每个指令),这可能会带来性能上的好处。
我们来举例说明两个选项:
选项1:
angular.module('app').directive('myDirective', function(){ function doSomethingFancy(el){ // In here we have our operations on the element } return { link: function(scope, element){ // Bind to the window resize event for each directive instance. angular.element(window).on('resize', function(){ doSomethingFancy(element); }); } }; });
选项2:
angular.module('app').directive('myDirective', function(){ function doSomethingFancy(){ var elements = document.querySelectorAll('[my-directive]'); angular.forEach(elements, function(el){ // In here we have our operations on the element }); } return { link: function(scope, element){ // Maybe we have to do something in here, maybe not. } }; // Bind to the window resize event only once. angular.element(window).on('resize', doSomethingFancy); });
这两种方法工作正常,但我觉得选项二不是真正的'Angular-ish'。
有任何想法吗?
我select了另一种方法来有效地本地化全局事件,如窗口大小调整。 它通过另一个指令将Javascript事件转换为Angular作用域事件。
app.directive('resize', function($window) { return { link: function(scope) { function onResize(e) { // Namespacing events with name of directive + event to avoid collisions scope.$broadcast('resize::resize'); } function cleanUp() { angular.element($window).off('resize', onResize); } angular.element($window).on('resize', onResize); scope.$on('$destroy', cleanUp); } } });
在基本情况下,可以使用哪些应用程序的根元素
<body ng-app="myApp" resize>...
然后在其他指令中听取事件
<div my-directive>....
编码为:
app.directive('myDirective', function() { return { link: function(scope, element) { scope.$on('resize::resize', function() { doSomethingFancy(element); }); }); } });
这比其他方法有许多好处:
-
关于如何使用指令的确切forms并不脆弱。 您的选项2需要
my-directive
时,angular度将以下内容视为等同:my:directive
,data-my-directive
,x-my-directive
,my_directive
可以在指令指南中看到 -
您有一个位置来影响JavaScript事件如何转换为Angular事件,然后影响所有的监听器。 说你以后想要debounce JavaScript
resize
事件,使用Lodash debounce函数 。 你可以修改resize
指令来:angular.element($window).on('resize', $window._.debounce(function() { scope.$broadcast('resize::resize'); },500));
-
因为它不一定会触发
$rootScope
的事件,所以只需移动放置resize
指令的位置,就可以将事件限制为仅部分应用程序<body ng-app="myApp"> <div> <!-- No 'resize' events here --> </div> <div resize> <!-- 'resize' events are $broadcast here --> </div>
-
您可以使用选项扩展指令,并在应用程序的不同部分以不同的方式使用它。 假设你想在不同的部分使用不同的去抖版本:
link: function(scope, element, attrs) { var wait = 0; attrs.$observe('resize', function(newWait) { wait = $window.parseInt(newWait || 0); }); angular.element($window).on('resize', $window._.debounce(function() { scope.$broadcast('resize::resize'); }, wait)); }
用作:
<div resize> <!-- Undebounced 'resize' Angular events here --> </div> <div resize="500"> <!-- 'resize' is debounced by 500 milliseconds --> </div>
-
稍后可以用其他可能有用的事件来扩展指令。 也许像
resize::heightIncrease
这样的东西。resize::heightDecrease
,resize::widthIncrease
,resize::widthDecrease
。 然后在应用程序中有一个位置可以处理记忆和处理窗口的确切尺寸。 -
您可以将数据与事件一起传递。 比如,您可能需要处理跨浏览器问题的视口高度/宽度(取决于您需要IE支持的距离,以及是否包含其他库来帮助您)。
angular.element($window).on('resize', function() { // From http://stackoverflow.com/a/11744120/1319998 var w = $window, d = $document[0], e = d.documentElement, g = d.getElementsByTagName('body')[0], x = w.innerWidth || e.clientWidth || g.clientWidth, y = w.innerHeight|| e.clientHeight|| g.clientHeight; scope.$broadcast('resize::resize', { innerWidth: x, innerHeight: y }); });
这给你一个地方稍后添加到数据。 比如说你想发送自上次去抖事件以来的尺寸差异? 你可能会添加一些代码来记住旧的尺寸,并发送差异。
从本质上讲,这种devise提供了一种方式,以一种可configuration的方式将全局JavaScript事件转换为本地Angular事件,而不仅仅是本地应用程序,而是本地到应用程序的不同部分,具体取决于指令的位置。
在框架之上进行开发时,在devise一个习惯用法之前,我经常会发现对于一个问题进行无知的思考是有帮助的。 回答“什么”和“为什么”会把“如何”赶出去。
这里的答案实际上取决于doSomethingFancy()
的复杂性。 是否存在与此指令的实例相关的数据,一组function或域对象? 它是一个纯粹的表象问题,就像调整某些元素的width
或height
属性到适当比例的窗口大小一样? 确保你使用正确的工具来完成这项工作; 当工作需要镊子时,不要拿起整个瑞士军刀,而且你可以独立使用一把。 为了继续下去,我将假设doSomethingFancy()
是纯粹的表示函数。
在Angular事件中包装全局浏览器事件的关注可以通过一些简单的运行阶段configuration来处理:
angular.module('myApp') .run(function ($rootScope) { angular.element(window).on('resize', function () { $rootScope.$broadcast('global:resize'); }) }) ;
现在Angular不需要为每个$digest
与指令相关的所有工作,而是获得相同的function。
第二个问题是当这个事件被触发时在n
个元素上进行操作。 再说一遍,如果你不需要所有指令的花里胡哨的话,还有其他的方法可以做到这一点。 您可以在上面的运行模块中扩展或调整方法:
angular.module('myApp') .run(function () { angular.element(window).on('resize', function () { var elements = document.querySelectorAll('.reacts-to-resize'); }) }) ;
如果确实需要在resize事件中发生更复杂的逻辑,它并不一定意味着一个或多个指令是处理它的最好方法。 你可以使用一个简单的中介服务来获得实例化,而不是上述的匿名运行阶段configuration:
/** * you can inject any services you want: $rootScope if you still want to $broadcast (in) * which case, you'd have a "Publisher" instead of a "Mediator"), one or more services * that maintain some domain objects that you want to manipulate, etc. */ function ResizeMediator($window) { function doSomethingFancy() { // whatever fancy stuff you want to do } angular.element($window).bind('resize', function () { // call doSomethingFancy() or maybe some other stuff }); } angular.module('myApp') .service('resizeMediator', ResizeMediator) .run(resizeMediator) ;
现在我们有一个可以进行unit testing的封装服务,但不会运行未使用的执行阶段。
一些关注也会影响到这个决定:
- 死听众 – 使用选项1,您至less为指令的每个实例创build一个事件侦听器。 如果这些元素被dynamic地添加到DOM中或从DOM中删除,并且不调用
$on('$destroy')
,那么当事件处理程序的元素不再存在时,您将面临事件处理程序自行应用的风险。 - 宽度/高度运算符的性能 – 假设全局事件是浏览器resize,我假设这里有盒子模型逻辑。 如果不是,请忽略这个; 如果是这样的话,你需要小心你正在访问的属性和频率,因为浏览器回stream可能是性能下降的罪魁祸首 。
很可能这个答案不像你所期望的那样是“angular度”,但是这是我解决这个问题的方式,因为我通过增加了盒子模型逻辑的假设来理解它。
在我看来,我会去方法#1和使用$窗口服务一点点调整。
angular.module('app').directive('myDirective', function($window){ function doSomethingFancy(el){ // In here we have our operations on the element } return { link: function(scope, element){ // Bind to the window resize event for each directive instance. anguar.element($window).bind('resize', function(){ doSomethingFancy(element); }); } }; });
#2参考这个方法,在这里稍微改变一下 – 你可以把这个事件监听器放在app.run的更高的地方 – 当事件发生的时候,你可以播放另外一个事件,当事件发生时。
编辑 :我越想到这个方法越多,我开始喜欢它的第一个…伟大的健壮的方式来听取窗口resize事件 – 也许在未来的东西需要“知道”这个信息以及除非你做了这样的事情,否则你不得不再次设置window.resize事件的另一个事件监听器。
app.run
app.run(function($window, $rootScope) { angular.element($window).bind('resize', function(){ $rootScope.$broadcast('window-resize'); }); }
指令 angular.module('app')。指令('myDirective',函数($ rootScope){
function doSomethingFancy(el){ // In here we have our operations on the element } return { link: function(scope, element){ // Bind to the window resize event for each directive instance. $rootScope.$on('window-resize', function(){ doSomethingFancy(element); }); } }; });
最后一个关于如何做的东西的一个很好的来源是跟随angularUI的人,例如ui-bootstrap 。 我学习了一堆如何从这些家伙的东西,例如学习unit testing的乐趣。 他们提供了一个伟大的干净的代码库结帐。
第二种方法感觉更脆弱,因为Angular提供了很多方法来引用模板中的指令( my-directive
, my_directive
, my:directive
, x-my-directive
, data-my-directive
等),所以CSSselect器覆盖他们都可能变得非常复杂。
如果您只在内部使用指令或者由单个单词组成,这可能不是什么大问题。 但是,如果其他开发人员(使用不同的编码约定)可能会使用您的指令,则可能需要避免使用第二种方法。
但我会很务实。 如果您正在处理less数情况,请使用#1。 如果你有几百个,我会去#2。
这里有一种方法可以做到,只需将你的元素存储在一个数组中,然后在“全局事件”中循环遍历元素,并执行你需要做的事情。
angular.module('app').directive('myDirective', function($window){ var elements = []; $window.on('resize', function(){ elements.forEach(function(element){ // In here we have our operations on the element }); }); return { link: function(scope, element){ elements.push(element); } }; });