AngularJS:绑定到指令中的全局事件的最佳方法是什么

时间:2014-04-24 14:41:25

标签: javascript angularjs events resize directive

想象一下AngularJS中你想要创建一个需要响应全局事件的指令的情况。在这种情况下,假设窗口调整大小事件。

最佳方法是什么?我看到它的方式,我们有两个选择: 1.让每个指令绑定到事件,并在当前元素上做它的魔力 2.创建一个全局事件侦听器,它执行DOM选择器以获取应该应用逻辑的每个元素。

选项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'。

有什么想法吗?

5 个答案:

答案 0 :(得分:137)

我选择了另一种方法来有效地本地化全局事件,例如窗口大小调整。它通过另一个指令将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);
      });
    });
  }
});

与其他方法相比,这有许多好处:

  • 对于如何使用指令的确切形式并不脆弱。当角度将以下内容视为等效时,您的选项2需要my-directivemy:directivedata-my-directivex-my-directivemy_directive,如guide for directives所示}

  • 您只有一个地方可以确切地影响Javascript事件如何转换为Angular事件,然后影响所有侦听器。假设您以后想要使用Lodash debounce function去除javascript resize事件。您可以将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::heightDecreaseresize::widthIncreaseresize::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
      });
    });
    

    它为您提供了一个稍后添加到数据的位置。例如。说你想发送自上次去抖事件以来的尺寸差异?您可以添加一些代码来记住旧的大小并发送差异。

本质上,这种设计提供了一种方法,可以以可配置的方式将全局Javascript事件转换为本地Angular事件,而不仅仅局部转换为应用程序,而是本地应用程序的不同部分,具体取决于指令的位置。

答案 1 :(得分:6)

在框架之上进行开发时,我经常发现在设计一个惯用之前,有必要对问题进行非常有用的思考。回答“什么”和“为什么”驱逐了“如何”。

这里的答案实际上取决于doSomethingFancy()的复杂性。是否存在与此指令实例关联的数据,一组功能或域对象?这是纯粹的表现性问题,比如将某些元素的widthheight属性调整为适当的窗口大小比例?确保你正在使用合适的工具;当工作需要镊子并且您可以使用独立配对时,不要携带整个瑞士军刀。为了继续这种方式,我将假设doSomethingFancy()是纯粹的表现函数。

在Angular事件中包装全局浏览器事件的问题可以通过一些简单的运行阶段配置来处理:

angular.module('myApp')
    .run(function ($rootScope) {
        angular.element(window).on('resize', function () {
            $rootScope.$broadcast('global:resize');  
        })
    })
;

现在,Angular不必在每个$digest上执行与指令相关的所有工作, 但是你得到了同样的功能。

第二个问题是在触发此事件时对n个元素进行操作。同样,如果你不需要指令的所有细节和哨声,还有其他方法可以实现这一点。您可以在上面的运行块中扩展或调整方法:

angular.module('myApp')
    .run(function () {
        angular.element(window).on('resize', function () {
            var elements = document.querySelectorAll('.reacts-to-resize');
        })
    })
;

如果你有更复杂的逻辑需要在resize事件上发生,它仍然不一定意味着一个或多个指令是处理它的最佳方法。您可以使用实例化的简单中介服务,而不是前面提到的匿名运行阶段配置:

/**
 * 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)
;

现在我们有一个封装服务,可以进行单元测试,但不会运行未使用的执行阶段。

一些关注因素也会影响决策:

  • 死侦听器 - 使用选项1,您将为该指令的每个实例创建至少一个事件侦听器。如果这些元素被动态地添加到DOM或从DOM中删除,并且您没有调用$on('$destroy'),那么当您的元素不再存在时,您将面临事件处理程序自我应用的风险。
  • 宽度/高度运算符的性能 - 假设全局事件是浏览器调整大小,我假设这里有盒模型逻辑。如果没有,请忽略这个;如果是这样,您需要注意您正在访问的属性以及频率,因为浏览器重排可能是huge culprit in performance degradation

这个答案很可能不像你希望的那样是“Angular”,但这是我解决问题的方式,正如我所理解的那样,增加了仅使用盒子模型逻辑的假设。

答案 2 :(得分:3)

在我看来,我会使用方法#1和使用$ window服务进行一些调整。

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 - 在这里,当事件发生时,你可以播放指令选择的另一个事件并做一些奇特的事情。事件发生。

编辑:我对这种方法的思考越多,我就越开始喜欢它的第一个...非常强大的方式来监听窗口调整大小事件 - 可能在未来的某些事情否则需要知道&#34;这个信息,除非你做这样的事情,你被迫设置 - 再次 - window.resize事件的另一个事件监听器。

<强> app.run

app.run(function($window, $rootScope) {
  angular.element($window).bind('resize', function(){
     $rootScope.$broadcast('window-resize');
  });
}

<强>指令     angular.module(&#39; app&#39;)指令(&#39; myDirective&#39;,函数($ 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);
             });
        }
    };
});

<强>最后 如何做事的一个很棒的来源是跟随angular-ui家伙,例如ui-bootstrap。我从这些家伙那里学到了很多东西,例如学习角度单元测试的乐趣。它们提供了一个非常干净的代码库来结账。

答案 3 :(得分:1)

第二种方法感觉更脆弱,因为Angular提供了很多方法来引用模板中的指令(my-directivemy_directivemy:directivex-my-directive,{{ 1}}等等,所以覆盖它们的CSS选择器可能会变得非常复杂。

如果您只在内部使用指令或者它们只包含一个单词,那么这可能不是什么大问题。但是,如果其他开发人员(具有不同的编码约定)可能正在使用您的指令,您可能希望避免使用第二种方法。

但我务实。如果您正在处理少数几个实例,请使用#1。如果你有数百个,我会选择#2。

答案 4 :(得分:1)

这是您可以做到的一种方式,只需将您的元素存储在一个数组中,然后在&#34;全局事件&#34; 中,您可以遍历元素并执行你需要做什么。

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);
        }
    };
});