【问题标题】:AngularJS 1.5+ Components do not support Watchers, what is the work around?AngularJS 1.5+ 组件不支持 Watchers,解决方法是什么?
【发布时间】:2016-06-02 17:57:31
【问题描述】:

我一直在将我的自定义指令升级到新的component architecture。我读过组件不支持观察者。它是否正确?如果是这样,您如何检测对象的变化?对于一个基本示例,我有一个自定义组件myBox,它有一个子组件游戏,并在游戏上绑定。如果游戏组件中有更改游戏,我如何在 myBox 中显示警报消息?我知道有 rxJS 方法是否可以纯粹以角度来做到这一点?我的JSFiddle

JavaScript

var app = angular.module('myApp', []);
app.controller('mainCtrl', function($scope) {

   $scope.name = "Tony Danza";

});

app.component("myBox",  {
      bindings: {},
      controller: function($element) {
        var myBox = this;
        myBox.game = 'World Of warcraft';
        //IF myBox.game changes, show alert message 'NAME CHANGE'
      },
      controllerAs: 'myBox',
      templateUrl: "/template",
      transclude: true
})
app.component("game",  {
      bindings: {game:'='},
      controller: function($element) {
        var game = this;


      },
      controllerAs: 'game',
      templateUrl: "/template2"
})

HTML

<div ng-app="myApp" ng-controller="mainCtrl">
  <script type="text/ng-template" id="/template">
    <div style='width:40%;border:2px solid black;background-color:yellow'>
      Your Favourite game is: {{myBox.game}}
      <game game='myBox.game'></game>
    </div>
  </script>

 <script type="text/ng-template" id="/template2">
    <div>
    </br>
        Change Game
      <textarea ng-model='game.game'></textarea>
    </div>
  </script>

  Hi {{name}}
  <my-box>

  </my-box>

</div><!--end app-->

【问题讨论】:

    标签: angularjs angularjs-directive angularjs-scope angularjs-components angularjs-1.5


    【解决方案1】:

    在没有Watchers 的情况下编写组件

    此答案概述了在不使用 watchers. 的情况下编写 AngularJS 1.5 组件的五种技术


    使用ng-change 指令

    哪些 alt 方法可用于在不使用 watch 的情况下观察 obj 状态变化以准备 AngularJs2?

    您可以使用ng-change 指令对输入更改做出反应。

    <textarea ng-model='game.game' 
              ng-change="game.textChange(game.game)">
    </textarea>
    

    并且要将事件传播到父组件,需要添加事件处理程序作为子组件的属性。

    <game game='myBox.game' game-change='myBox.gameChange($value)'></game>
    

    JS

    app.component("game",  {
          bindings: {game:'=',
                     gameChange: '&'},
          controller: function() {
            var game = this;
            game.textChange = function (value) {
                game.gameChange({$value: value});
            });
    
          },
          controllerAs: 'game',
          templateUrl: "/template2"
    });
    

    并且在父组件中:

    myBox.gameChange = function(newValue) {
        console.log(newValue);
    });
    

    这是今后的首选方法。 AngularJS 使用$watch 的策略是不可扩展的,因为它是一种轮询策略。当$watch 的听众数量达到 2000 左右时,UI 变得迟缓。 Angular 2 中的策略是使框架更具响应性,并避免将$watch 放在$scope 上。


    使用$onChanges 生命周期钩子

    版本 1.5.3 中,AngularJS 将 $onChanges 生命周期挂钩添加到 $compile 服务。

    来自文档:

    控制器可以提供以下方法作为生命周期钩子:

    • $onChanges(changesObj) - 每当单向 (&lt;) 或插值 (@) 绑定更新时调用。 changesObj 是一个散列,其键是已更改的绑定属性的名称,值是{ currentValue: ..., previousValue: ... } 形式的对象。使用此钩子触发组件内的更新,例如克隆绑定值以防止外部值的意外突变。

    — AngularJS Comprehensive Directive API Reference -- Life-cycle hooks

    $onChanges 钩子用于通过&lt; 单向绑定对组件的外部更改做出反应。 ng-change 指令用于通过 &amp; 绑定从组件外部的 ng-model 控制器传播更改。


    使用$doCheck 生命周期钩子

    版本 1.5.8 中,AngularJS 将 $doCheck 生命周期挂钩添加到 $compile 服务。

    来自文档:

    控制器可以提供以下方法作为生命周期钩子:

    • $doCheck() - 在摘要循环的每一轮调用。提供检测变化并采取行动的机会。必须从这个钩子中调用您为响应检测到的更改而希望采取的任何操作;实现这一点对何时调用 $onChanges 没有影响。例如,如果您希望执行深度相等检查,或检查 Date 对象,Angular 的更改检测器不会检测到更改,因此不会触发$onChanges,则此挂钩可能很有用。这个钩子是不带参数调用的;如果检测到更改,则必须存储以前的值以与当前值进行比较。

    — AngularJS Comprehensive Directive API Reference -- Life-cycle hooks


    require的组件间通信

    指令可以require其他指令的控制器来实现彼此之间的通信。这可以通过为require 属性提供对象映射在组件中实现。对象键指定所需的控制器(对象值)将在其下绑定到所需组件的控制器的属性名称。

    app.component('myPane', {
      transclude: true,
      require: {
        tabsCtrl: '^myTabs'
      },
      bindings: {
        title: '@'
      },
      controller: function() {
        this.$onInit = function() {
          this.tabsCtrl.addPane(this);
          console.log(this);
        };
      },
      templateUrl: 'my-pane.html'
    });
    

    欲了解更多信息,请参阅AngularJS Developer Guide - Intercomponent Communicatation


    使用RxJS 从服务推送值

    例如,如果您的服务处于保持状态,那该怎么办。我怎样才能将更改推送到该服务,页面上的其他随机组件才能知道这种更改?最近一直在努力解决这个问题

    使用RxJS Extensions for Angular 构建服务。

    <script src="//unpkg.com/angular/angular.js"></script>
    <script src="//unpkg.com/rx/dist/rx.all.js"></script>
    <script src="//unpkg.com/rx-angular/dist/rx.angular.js"></script>
    
    var app = angular.module('myApp', ['rx']);
    
    app.factory("DataService", function(rx) {
      var subject = new rx.Subject(); 
      var data = "Initial";
    
      return {
          set: function set(d){
            data = d;
            subject.onNext(d);
          },
          get: function get() {
            return data;
          },
          subscribe: function (o) {
             return subject.subscribe(o);
          }
      };
    });
    

    然后只需订阅更改即可。

    app.controller('displayCtrl', function(DataService) {
      var $ctrl = this;
    
      $ctrl.data = DataService.get();
      var subscription = DataService.subscribe(function onNext(d) {
          $ctrl.data = d;
      });
    
      this.$onDestroy = function() {
          subscription.dispose();
      };
    });
    

    客户端可以使用DataService.subscribe 订阅更改,生产者可以使用DataService.set 推送更改。

    DEMO on PLNKR

    【讨论】:

    • 谢谢 我希望对 myBox.game 而不是 game.gameChange 的更改做出反应。由于这不是输入而是标签,因此上述方法可能不起作用。我想我最终可能不得不求助于rxjs......
    • 我添加了将事件传播到父组件的信息。
    • 这个东西将如何处理myBox.game 变量上的编程值变化?
    • 很好的答案@georgeawg。例如,在您有一个处于保持状态的Service 的情况下呢?我怎样才能将更改推送到该服务,页面上的其他随机组件才能知道这种更改?最近一直在努力解决这个问题...
    • 很好的答案,只说你可以使用 rx.BehaviorSubject() 而不是 rx.Subject() 来改进你的 RxJS 解决方案,它自己存储最后一个值,并且可以初始化与您在服务中所做的一样使用默认值
    【解决方案2】:

    $watch 对象在$scope 对象中可用,因此您需要在控制器工厂函数中添加$scope,然后将观察者放在变量上。

    $scope.$watch(function(){
        return myBox.game;
    }, function(newVal){
       alert('Value changed to '+ newVal)
    });
    

    Demo Here

    注意:我知道您已将directive 转换为component,以消除对$scope 的依赖,这样您就更接近了 角2。但它似乎并没有因为这种情况而被删除。

    更新

    基本上 angular 1.5 确实添加了.component 方法,只是区分了两种不同的功能。像component.代表通过添加selector 来执行特定的行为,而directive 代表向DOM 添加特定的行为。指令只是.directive DDO(指令定义对象)上的包装方法。只有你能看到的是,他们在使用.component 方法时删除了link/compile 函数,你可以在其中获得角度编译的DOM。

    请务必使用 Angular 组件生命周期钩子的 $onChanges/$doCheck 生命周期钩子,这些将在 Angular 1.5.3+ 版本之后可用。

    $onChanges(changesObj) - 每当绑定更新时调用。 changesObj 是一个散列,其键是绑定属性的名称。

    $doCheck() - 当绑定更改时,在摘要循环的每一轮调用。提供检测变化并采取行动的机会。

    通过在组件内使用相同的功能将确保您的代码兼容以迁移到 Angular 2。

    【讨论】:

    • 谢谢你,我想现在会坚持下去。您能否指出任何链接,讨论哪些 alt 方法可用于观察 obj 状态变化,而无需使用 watch 来为 AngularJs2 做准备?
    • @KaTech 现在我只能说使用RxJSobservable,这样可以很好地兼容Angular2
    【解决方案3】:

    对于任何对我的解决方案感兴趣的人,我最终会求助于 RXJS Observables,当您使用 Angular 2 时,您将不得不使用它。这是一个用于组件之间通信的工作小提琴,它让我可以更好地控制要做什么观看。

    JS FIDDLE RXJS Observables

    class BoxCtrl {
        constructor(msgService) {
        this.msgService = msgService
        this.msg = ''
    
        this.subscription = msgService.subscribe((obj) => {
          console.log('Subscribed')
          this.msg = obj
        })
        }
    
      unsubscribe() {
        console.log('Unsubscribed')
        msgService.usubscribe(this.subscription)
      }
    }
    
    var app = angular
      .module('app', ['ngMaterial'])
      .controller('MainCtrl', ($scope, msgService) => {
        $scope.name = "Observer App Example";
        $scope.msg = 'Message';
        $scope.broadcast = function() {
          msgService.broadcast($scope.msg);
        }
      })
      .component("box", {
        bindings: {},
        controller: 'BoxCtrl',
        template: `Listener: </br>
        <strong>{{$ctrl.msg}}</strong></br>
        <md-button ng-click='$ctrl.unsubscribe()' class='md-warn'>Unsubscribe A</md-button>`
      })
      .factory('msgService', ['$http', function($http) {
        var subject$ = new Rx.ReplaySubject();
        return {
          subscribe: function(subscription) {
            return subject$.subscribe(subscription);
          },
          usubscribe: function(subscription) {
            subscription.dispose();
          },
          broadcast: function(msg) {
            console.log('success');
            subject$.onNext(msg);
          }
        }
      }])
    

    【讨论】:

      【解决方案4】:

      关于使用 ng-change 的小提示,建议与接受的答案一起使用,以及角度 1.5 组件。

      如果您需要监视ng-modelng-change 不起作用的组件,您可以将参数传递为:

      使用组件的标记:

      <my-component on-change="$ctrl.doSth()"
                    field-value="$ctrl.valueToWatch">
      </my-component>
      

      组件js:

      angular
        .module('myComponent')
        .component('myComponent', {
          bindings: {
            onChange: '&',
            fieldValue: '='
          }
        });
      

      组件标记:

      <select ng-model="$ctrl.fieldValue"
              ng-change="$ctrl.onChange()">
      </select>
      

      【讨论】:

        【解决方案5】:

        在 IE11 中可用,MutationObserver https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver。您需要将 $element 服务注入到控制器中,这会半破坏 DOM/控制器分离,但我觉得这是 angularjs 中的一个基本异常(即缺陷)。由于隐藏/显示是异步的,我们需要显示回调,而 angularjs 和 angular-bootstrap-tab 不提供。它还要求你知道你想观察哪个特定的 DOM 元素。我使用以下代码用于 angularjs 控制器来触发 Highcharts 图表重排。

        const myObserver = new MutationObserver(function (mutations) {
            const isVisible = $element.is(':visible') // Requires jquery
            if (!_.isEqual(isVisible, $element._prevIsVisible)) { // Lodash
                if (isVisible) {
                    $scope.$broadcast('onReflowChart')
                }
                $element._prevIsVisible = isVisible
            }
        })
        myObserver.observe($element[0], {
            attributes: true,
            attributeFilter: ['class']
        })
        

        【讨论】:

        • 为了更轻松地迁移到 Angular2+,请避免使用 $scope$rootScope。考虑使用RxJS 代替事件发射器和订阅者。
        【解决方案6】:

        真的很好接受的答案,但我可能会补充一点,你也可以使用事件的力量(如果你愿意的话,有点像 Qt 信号/插槽)。

        广播一个事件:$rootScope.$broadcast("clickRow", rowId) 由任何父母(甚至儿童控制器)。 然后在您的控制器中,您可以像这样处理事件:

        $scope.$on("clickRow", function(event, data){
            // do a refresh of the view with data == rowId
        });
        

        您也可以像这样添加一些日志记录(取自这里:https://stackoverflow.com/a/34903433/3147071

        var withLogEvent = true; // set to false to avoid events logs
        app.config(function($provide) {
            if (withLogEvent)
            {
              $provide.decorator("$rootScope", function($delegate) {
                var Scope = $delegate.constructor;
                var origBroadcast = Scope.prototype.$broadcast;
                var origEmit = Scope.prototype.$emit;
        
                Scope.prototype.$broadcast = function() {
                  console.log("$broadcast was called on $scope " + this.$id + " with arguments:",
                             arguments);
                  return origBroadcast.apply(this, arguments);
                };
                Scope.prototype.$emit = function() {
                  console.log("$emit was called on $scope " + this.$id + " with arguments:",
                             arguments);
                  return origEmit.apply(this, arguments);
                };
                return $delegate;
              });
            }
        });
        

        【讨论】:

        • 使用 Angular 2+,事件总线将消失(事件总线存在性能问题。)为了更容易迁移到 Angular2+,请避免使用 $scope$rootScope。考虑使用RxJS 代替事件发射器和订阅者。
        【解决方案7】:

        我迟到了。但它可以帮助其他人。

        app.component("headerComponent", {
            templateUrl: "templates/header/view.html",
            controller: ["$rootScope", function ($rootScope) {
                let $ctrl = this;
                $rootScope.$watch(() => {
                    return $ctrl.val;
                }, function (newVal, oldVal) {
                    // do something
                });
            }]
        });
        

        【讨论】:

        • 您能解释一下为什么会这样吗?滥用 rootscope 不是一个坏主意吗?
        猜你喜欢
        • 2012-04-14
        • 2020-10-14
        • 1970-01-01
        • 2014-03-16
        • 2011-03-12
        • 1970-01-01
        • 1970-01-01
        • 2020-10-02
        • 2023-02-20
        相关资源
        最近更新 更多