我怎样才能$watch看到Angular服务的属性变化呢?

How can I $watch for changes to properties of an Angular service?

本文关键字:服务 属性 变化 Angular 看到 watch      更新时间:2023-09-26

我怀疑答案是"你不能" -但也许你可以给我一个更好的方法。

考虑这个非常简单的游戏。你点击一个按钮,它会记录你点击了多少次。但最终,它会变得复杂得多——所以我有一个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>

到目前为止,一切顺利。这是我遇到的问题:我想添加一个成就系统,当玩家完成特定目标时(游戏邦注:例如,点击5次按钮),就会获得徽章。看起来它属于它自己的组件,所以我创建了第二个名为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>标记,作为实例化该范围并使其在应用程序的生命周期中保持活跃的一种侧门方法。我见过几次推荐这种方法,但感觉像是对指令的误用。一定有更干净的方法

  • 每当timesClicked增加时,在根作用域上设置Player $broadcast事件,并在Achievements中侦听该事件。我不喜欢这样。随着游戏变得越来越复杂,它最终会出现许多广播噪音。无论如何,这是旧的"让它全球化"的解决方案-有效,但从长远来看通常不是一个好主意:)

  • 在成就中,使用$rootScope.$new()创建一个新的特设范围,将Player连接到它,然后将$watch连接到它。(或者我可以在Player中创建新的作用域,然后将该作用域作为svc的属性公开。)老实说,我不确定这是否有效——但是当我发现自己在做这样的事情时,我怀疑我需要回溯和重构一些东西。

是否还有其他方法我忽略了?是否有一种方法重构我的应用程序,使timesClicked存储在更容易$watch的地方(但仍然容易在应用程序的不同部分之间共享)?

抱歉,如果我错过了一些超级明显的东西;我已经盯着Angular代码看了一整天了,我可能需要休息一下:)

Update:我应该提到,从MainController内注册$watch将不起作用。尽管我保持了这个演示代码的简单性,但实际的应用程序是一个使用ngRoute的SPA——所以如果播放器导航到不同的路径,MainController将被卸载。但是成绩仍然应该被监控和奖励。

一个简单而有效的方法是使用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);
    };
}]);

你可能还想在播放器服务中添加一种方法来取消订阅timesClicked更改

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