howdylikes

Google Developersってわかりづらいよね

AngularJS 変更監視 Scope#$watch、 Scope#$watchGroup、Scope#$watchCollection

リファレンス
https://code.angularjs.org/1.4.7/docs/api/ng/type/$rootScope.Scope#$watch

$watch

1つのプロパティを監視する
$watch(watchExpression, listener, [objectEquality]);
objectEqualityをtrueで指定するとangular.equalsを使用する

angular.module('myapp', [])
    .run(function() {
        console.log("myapp.run");
    })
    .controller('myController', function ($scope) {

        // $watch
        $scope.message = 'Hello, World!';
        $scope.$watch('message', function (newValue, oldValue, scope) {
            console.log('$scope.$watch(message)', newValue, oldValue);
        });
    });
}
// console
myapp.run
$scope.$watch(message) Hello, World! Hello, World!

変更をしなくても初期状態の設定だけで呼ばれる模様、ただしrunの後。

$watchGroup

複数のプロパティを監視する
$watchGroup(watchExpressions, listener);
いずれかが変わったら指定されたfunctionが呼ばれる感じ。

        // $watchGroup
        $scope.$watchGroup([
            'message',
            'message2'
        ], function (newValue, oldValue, scope) {
            console.log('$scope.$watchGroup([message])');
        });

$watchCollection

配列やオブジェクトを監視する用?
$watchCollection(obj, listener);
1階層だけ変更を監視します。
配列なら配列の要素の追加や削除は引っかかるがプロパティの変更は引っかからない。
オブジェクトならプロパティの変更が引っかかる。

        // $watchCollection (対象が配列)
        $scope.messages = [{"message":"hoge1"}, {"message":"hoge2"}];
        $scope.$watchCollection('messages', function (newValue, oldValue, scope) {
            console.log('$scope.$watchCollection(messages)');
        });

        this.pushMessage = function() {
            $scope.messages.push({"message":"hogex"}); // ひっかかる
        }
        this.popMessage = function() {
            $scope.messages.pop();  // ひっかかる
        }
        this.changePropertyMessage = function() {
            $scope.messages[$scope.messages.length-1].message = 'change';  // ひっかからない
        }

        // $watchCollection (対象がオブジェクト)
        $scope.messageinfo = {type: 'type'};
        this.messageinfo = {type: 'typo'};

        $scope.$watchCollection('messageinfo', function (newValue, oldValue, scope) {
            console.log('$scope.$watchCollection(messageinfo)'); // ひっかかる
        });

その他

上記例では変更監視対象を文字列で指定していました。
この場合$scopeの文字列のプロパティを監視するということです。

また、文字列の代わりにfunctionで変更監視対象を直接指定することが可能です。
その場合$scopeに深い意味はないです。監視対象にするための機能を呼び出しているようなものでしょうか。
そのため$rootScopeで対象オブジェクトを渡しても動作します。
これを使えばController asを使っている場合に$scopeをControllerから排除できそうです。

以下のプログラムは何が表示されるでしょうか。

angular.module('myapp', [])
    .run(function() {
        console.log("myapp.run");
    })
    .controller('myController', function ($scope, $rootScope) {

        var self = this;
        this.message = 'Hello, World!';
        $scope.$watch(
            'message', function (newValue, oldValue, scope) {
                console.log('これは呼ばれる?1');
            });
        $rootScope.$watch(
            'notDefinition', function (newValue, oldValue, scope) {
                console.log('これは呼ばれる?2');
            });
        $rootScope.$watch(
            function () {
                return self.message;
            }, function (newValue, oldValue, scope) {
                console.log('これは呼ばれる?3');
                $scope.message = '$scope.message' + new Date();
            });

        this.click = function() {
            this.message = 'hoge' + new Date();
        }
    });
// console
myapp.run
これは呼ばれる?2
これは呼ばれる?3
これは呼ばれる?1

// this.clickを呼ぶ
これは呼ばれる?3
これは呼ばれる?1

どうも定義順ではなく$rootScopeから(親Scopeから?)呼ばれるようです。
またそもそもプロパティが未定義でも最初に呼ばれるというのはちょっと不思議な感じがしますが
$watchは定義時と変更時に呼ばれると覚えれば良さそうです。

また指定したプロパティを監視する定義をしているだけでrun終了時にプロパティが存在していなくても問題なく、見に行ったけどundefinedだったと記憶しているようなイメージです。
そのためoldValueもnewValueもundefined→変わっていない。
上記で言うとscope.message = '$scope.message' + new Date(); でプロパティを設定した段階でoldValueがundefinedでnewValueに文字列が設定された→変わった。という判断をしているのでしょう。。多分。