如何监视Angular服务属性的更改?

时间:2015-09-04 02:01:46

标签: javascript angularjs

我怀疑答案是“你不能” - 但也许你可以向我展示一个更好的方法。

考虑这个非常简单的游戏。您单击一个按钮,它会跟踪您单击的次数。但最终会更加复杂 - 所以我有Player服务,其中包含timesClicked值。

angular.module( 'ClickGame', [] )

angular.module('ClickGame').factory( 'Player', function() {
    var svc = {};
    svc.timesClicked = 0;
    return svc;
} );

angular.module('ClickGame').controller( 'MainController', [ '$scope', 'Player', function( $scope, Player ) {

    // In practice, I wouldn't necessarily expose Player directly on the
    // scope like this, but it makes for simpler demo code.
    $scope.Player = Player;

    $scope.click = function() {
        Player.timesClicked++;
    };

} ] );

HTML非常明显:

<html ng-app="ClickGame">

<body ng-controller="MainController">

    <p><button type="button" ng-click="click()">Click me!</button></p>

    <p>You have clicked {{ Player.timesClicked }} times.</p>

    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.15/angular.min.js"></script>
    <script src="demo.js"></script>

</body>

</html>

到目前为止,这么好。这就是我遇到问题的地方:我想添加一个成就系统,当他们完成某些目标(比如点击按钮五次)时,会向玩家颁发徽章。这似乎属于它自己的组件,所以我创建了一个名为Achievements的第二个服务,它注入了Player

app.factory( 'Achievements', [ 'Player', function( Player ) {

    // I can't do this, because neither Achievements nor Player have a scope.
    $scope.$watch( 'Player.timesClicked', function( newValue, oldValue ) {
        if ( newValue >= 5 ) {
            console.log( 'ACHIEVEMENT UNLOCKED: Clicked 5 Times' );
            // TODO: unwatch this model, to conserve resources and prevent
            // the achievement from being awarded more than once
        }
    } );

} ] );

看到问题?

我考虑并拒绝的方法:

  • 使用$interval定期轮询Player.timesClicked。由于一系列原因,这显然很可怕。

  • 使Achievements成为指令而不是服务,因为指令确实有范围。但是,这需要我在我的index.html中放置一个<achievement>标记,作为实例化该范围并在应用程序生命周期内保持活动状态的一种侧门方法。我已经看过这种方法推荐了几次,但感觉就像滥用指令一样。必须有一个更清洁的方式。

  • 只要Player递增,就会在$broadcast timesClicked根目录上发生事件,并在Achievements中监听该事件。我不喜欢这个。随着游戏的复杂性增加,它最终会产生大量的广播噪音。无论如何,这是旧的“只做全球化”的解决方案 - 有效,但从长远来看通常不是一个好主意:)

  • 在“成就”中,使用$rootScope.$new()创建新的临时范围,将Player附加到该范围,然后$watch。 (或者我可以在Player中创建新范围,然后将该范围公开为svc的属性。)老实说,我不确定这是否会起作用 - 但是当我发现自己在做什么时像这样的东西,我怀疑我需要回溯并重构一些东西。

我还有另一种方法吗?有没有办法重构我的应用程序,以便timesClicked存储在一个更容易$watch的地方(但仍然很容易在我的应用程序的不同部分之间共享)?

对不起,如果我错过了一些非常明显的东西;我整天都在盯着Angular代码,我可能需要休息一下:)

更新:我应该提及从$watch内注册MainController将无效。虽然我保持这个演示代码很简单,但实际的应用程序是使用ngRoute的SPA - 因此如果玩家导航到不同的路线,MainController将被卸载。但是仍然应该监测和奖励成就。

2 个答案:

答案 0 :(得分:1)

一种简单而有效的方法是使用pub / sub方法,但使用回调而不是事件。这就是我要做的事情:

<html ng-app="ClickGame">
<body ng-controller="MainController">
    <p><button type="button" ng-click="click()">Click me!</button></p>
    <p>You have clicked {{ timesClicked }} times.</p>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.15/angular.min.js"></script>
    <script src="demo.js"></script>
</body>
</html>

angular.module( 'ClickGame', [] )
.factory( 'Player', function() {
    var timesClicked = 0, timesClickedChangedCallbacks = [];


    return {
        getTimesClicked: function() { return timesClicked; },
        setTimesClicked: function(val) {
            timesClicked = val;
            timesClickedChangedCallbacks.forEach(function(cb) { cb(val); });
        },
        onTimesClickedChanged: function(cb) {
            if (cb && typeof cb == 'function') {
                timesClickedChangedCallbacks.push(cb);
            }
        }
    };
})
.factory( 'Achievements', [ 'Player', function( Player ) {
    Player.onTimesClickedChanged(function(val) {
        if ( val == 5 ) {
            console.log( 'ACHIEVEMENT UNLOCKED: Clicked 5 Times' );
        }
    });
} ] )
.controller( 'MainController', [ '$scope', 'Player', function( $scope, Player ) {
    $scope.timesClicked = Player.getTimesClicked();

    $scope.click = function() {
        $scope.timesClicked++;
        Player.setTimesClicked($scope.timesClicked);
    };
}]);

您可能还想添加一种方法来取消订阅播放器服务中的次点击更改。

答案 1 :(得分:0)

    Inject Achievement Service in controller and put this service in your scope

    app.controller('MainController' , ['Achievements' ,function(Achievements){

    $scope.achivement = Achievements;  //Put this service in your scope
       $scope.$watch('achivement.Player.links',function(newVal,oldVal){
       //do your things
    }); 
   }])