AngularJS でのダーティチェック

Rana Hasnain Khan 2022年5月31日
AngularJS でのダーティチェック

AngularJs でダーティチェックを行う方法を紹介します。

AngularJS にダーティチェックを実装する

AngularJS は、双方向データバインディングの $scope 変数に対してダーティチェックを実行します。Ember.js は、セッターとゲッターをプログラムで修正することで双方向のデータバインディングを実行しますが、Angular では、ダーティチェックにより、使用可能かどうかに関係なく変数を検索できます。

# AngularJs
$scope.$watch( wExp, listener, objEq );

$scope.$watch 関数は、変数がいつ変更されたかを確認したい場合に使用されます。この関数を適用するには、3つの引数を選択する必要があります。wExp は監視したいもの、listener は更新時に実行したいこと、変数とオブジェクトのどちらをチェックするかです。

変数をチェックしたいときにこの関数を使用している間はこれをスキップできます。以下に示す例を見てみましょう。

#AngularJs
$scope.newName = 'Subhan';

$scope.$watch( function( ) {
    return $scope.name;
}, function( newVal, oldVal ) {
    console.log('$scope.newName is updated!');
} );

Angular は、ウォッチャー機能を $scope に保存します。これは、$scope をコンソールに記録することで確認できます。

$watch では、関数の代わりに文字列を利用することもできます。これも関数が機能するのと同じように機能します。

Angular ソースコードに文字列を追加すると、コードは次のようになります。

#AngularJs
if (typeof wExp == 'string' && get.constant) {
  var newFn = watcher.fn;
  watcher.fn = function(newVal, oldVal, scope) {
    newFn.call(this, newVal, oldVal, scope);
    arrayRemove(array, watcher);
  };
}

このコードを適用することにより、wExp が関数として設定されます。これにより、選択した名前の変数でリスナーが指定されます。

AngularJS の $watchers 関数

選択したすべてのウォッチャーは、$scope$$watchers 変数に保存されます。以下に示すように、$$watchers をチェックすると、オブジェクトの配列が見つかります。

#AngularJs
$$watchers = [
    {
        eq: false,
        fn: function( newVal, oldVal ) {},
        last: 'Subhan',
        exp: function(){},
        get: function(){}
    }
];

unRegWatch 関数は $watch 関数によって返されます。これは、最初の $scope.$watch を変数に割り当てたい場合、監視を停止するように命令することで簡単にこれを達成できることを示しています。

ウォッチャーを切断する前に、ログに記録された最初の $scope を開いて確認したことを確認してください。

AngularJS の $scope.$apply 関数

controller/directive/etc. を実行しようとすると、Angular は $scope.$watch を呼び出す関数を実行します。rootScope$digest 関数を管理する前に、$apply 関数は選択した関数を実行します。

Angular の $apply 関数は次のとおりです。

#AngularJs
$apply: function(expr) {
    try {
      beginPhase('$apply');
      return this.$eval(expr);
    } catch (e) {
      $exceptionHandler(e);
    } finally {
      clearPhase();
      try {
        $rootScope.$digest();
      } catch (e) {
        $exceptionHandler(e);
        throw e;
      }
    }
}

AngularJS の expr 引数

$scope.$apply を使用する場合、場合によっては Angular を使用する場合、または関数を渡す場合、expr 引数はその関数です。

この機能を使用している間、$apply を最も頻繁に使用する必要はありません。ng-keydown$scope.$apply を使用するとどうなるか見てみましょう。

ディレクティブを送信するには、次のコードを使用します。

#AngularJs
var EventNgDirective = {};
forEach(
  keydown keyup keypress submit focus blur copy cut paste'.split(' '),
  function(newName) {
    var newDirectiveName = newDirectiveNormalize('ng-' + newName);
    EventNgDirective[newDirectiveName] = ['$parse', function($parse) {
      return {
        compile: function($element, attr) {
          var fn = $parse(attr[directiveName]);
          return function ngEventHandler(scope, element) {
            element.on(lowercase(newName), function(event) {
              scope.$apply(function() {
                fn(scope, {$event:event});
              });
            });
          };
        }
      };
    }];
  }
);

このコードは、発生する可能性のある対照的なタイプのイベントをループし、ng(eventName) と呼ばれる新しいディレクティブを生成します。ディレクティブのコンパイル機能が関係する場合、イベントハンドラーが要素に記録され、イベントにはディレクティブの名前が含まれます。

このイベントが発生すると、$scope.$apply が Angular によって実行され、実行する機能が提供されます。

このようにして、$scope 値が要素値で更新されますが、このバインディングは一方向のバインディングにすぎません。この理由は、ng-keydown を適用したためです。これにより、このイベントが適用された場合にのみ変更できます。

その結果、新しい値が得られます!

私たちの主な目標は、双方向のデータバインディングを実現することです。この目標を達成するために、ng-model を使用できます。

ng-model が機能するために、$scope.$watch$scope.$apply の両方を利用します。

私たちが与えた入力は、ng-model によってイベントハンドラーに参加します。この時点で、$scope.$watch が呼び出されます。

$scope.$watch は、ディレクティブのコントローラーで呼び出されます。

#AngularJs
$scope.$watch(function ngModelWatch() {
    var value = ngModelGet($scope);

    if (ctrl.$modelValue !== value) {

        var formatters = ctrl.$formatters,
            idx = formatters.length;

        ctrl.$modelValue = value;
        while(idx--) {
            value = formatters[idx](value);
        }

        if (ctrl.$viewValue !== value) {
            ctrl.$viewValue = value;
            ctrl.$render();
        }
    }

    return value;
});

$scope.$watch の設定時に引数を 1つだけ使用すると、更新されているかどうかに関係なく、選択した関数が適用されます。ng-model にある関数は、モデルとビューが同期しているかどうかを調べます。

同期していない場合、関数はモデルに新しい値を与えて更新します。この関数を使用すると、$digest でこの関数を実行したときに新しい値を返すことで、新しい値を知ることができます。

リスナーが解雇されない理由

説明したのと同様の関数で $scope.$watch 関数の登録を解除した例のポイントに戻りましょう。 $scope.name を更新しても通知が届かない理由を確認できるようになりました。

ご存知のとおり、$scope.$apply は、各ディレクティブコントローラ関数で Angular によって実行されます。 $scope.$apply 関数は、ディレクティブのコントローラー関数を推定した 1つの条件でのみ $digest を実行します。

これは、実行できるようになる前に $scope.$watch 関数の登録を解除したことを意味します。したがって、呼び出される可能性は最小限またはまったくありません。

AngularJS の $digest 関数

この関数は、$scope.$apply によって $rootScope で呼び出すことができます。 $rootScope でダイジェストサイクルを実行すると、スコープを通過し、代わりにダイジェストサイクルを実行します。

ダイジェストサイクルは、$$watchers 変数のすべての wExp 関数を起動します。最後の既知の値に対してそれらをチェックします。

結果が否定的な場合、リスナーは解雇されます。

ダイジェストサイクルでは、実行時に 2 回ループします。一度、ウォッチャー間でループし、サイクルがダーティでなくなるまで再びループします。

wExp と最後の既知の値が同等でない場合、サイクルがダーティであると言います。通常、このサイクルは 1 回実行され、10 回を超えるとエラーが発生します。

Angular は、モデル値が変更される可能性のあるすべてのものに対して $scope.$apply を実行できます。

$scope.$apply(); を実行する必要があります Angular の外で $scope を更新したとき。これにより、スコープが更新されたことが Angular に通知されます。

ダーティチェックの基本バージョンを設計しましょう。

保存したいすべてのデータは、Scope 機能に含まれます。関数のオブジェクトを展開して、$digest$watch を複製します。

Scope に関連する機能を評価する必要がないため、$apply は使用しません。 $digest を直接使用します。

Scope は次のようになります。

#AngularJs
var Scope = function( ) {
    this.$$watchers = [];
};

Scope.prototype.$watch = function( ) {

};

Scope.prototype.$digest = function( ) {

};

$watch が採用する必要がある 2つのパラメータ、wExplistener があります。Scope で値を設定できます。

$watch が呼び出されると、これらは以前に Scope に保存されていた $$watcher 値に送信されます。

#AngularJs
var Scope = function( ) {
    this.$$watchers = [];
};

Scope.prototype.$watch = function( wExp, listener ) {
    this.$$watchers.push( {
        wExp: wExp,
        listener: listener || function() {}
    } );
};

Scope.prototype.$digest = function( ) {

};

ここで、listener が空の関数に設定されていることがわかります。

この例に従うことで、listener が提供されていない場合、すべての変数に $watch を簡単に登録できます。

次に、$digest を操作します。古い値と新しい値が同等であるかどうかを調べることができます。

リスナーがまだ起動されていない場合は、リスナーを起動することもできます。これを実現するために、同等になるまでループします。

dirtyChecking 変数は、値が等しいかどうかに関係なく、この目標を達成するのに役立ちました。

#AngularJs
var Scope = function( ) {
    this.$$watchers = [];
};

Scope.prototype.$watch = function( wExp, listener ) {
    this.$$watchers.push( {
        wExp: wExp,
        listener: listener || function() {}
    } );
};

Scope.prototype.$digest = function( ) {
    var dirtyChecking;

    do {
            dirtyChecking = false;

            for( var i = 0; i < this.$$watchers.length; i++ ) {
                var newVal = this.$$watchers[i].wExp(),
                    oldVal = this.$$watchers[i].last;

                if( oldVal !== newVal ) {
                    this.$$watchers[i].listener(newVal, oldVal);

                    dirtyChecking = true;

                    this.$$watchers[i].last = newVal;
                }
            }
    } while(dirtyChecking);
};

次に、スコープの新しい例を設計します。これを $scope に割り当てます。次に、監視機能を登録します。

次に、それを更新してダイジェストします。

#AngularJs
var Scope = function( ) {
    this.$$watchers = [];
};

Scope.prototype.$watch = function( wExp, listener ) {
    this.$$watchers.push( {
        wExp: wExp,
        listener: listener || function() {}
    } );
};

Scope.prototype.$digest = function( ) {
    var dirtyChecking;

    do {
            dirtyChecking = false;

            for( var i = 0; i < this.$$watchers.length; i++ ) {
                var newVal = this.$$watchers[i].wExp(),
                    oldVal = this.$$watchers[i].last;

                if( oldVal !== newVal ) {
                    this.$$watchers[i].listener(newVal, oldVal);

                    dirtyChecking = true;

                    this.$$watchers[i].last = newVal;
                }
            }
    } while(dirtyChecking);
};


var $scope = new Scope();

$scope.newName = 'Subhan';

$scope.$watch(function(){
    return $scope.newName;
}, function( newVal, oldVal ) {
    console.log(newVal, oldVal);
} );

$scope.$digest();

ダーティチェックの実装に成功しました。コンソールを見ると、ログが表示されます。

#AngularJs
Subhan undefined

以前は、$scope.newName は定義されていませんでした。Subhan 用に修正しました。

$digest 関数を入力の keyup イベントに接続します。これにより、自分で指定する必要がなくなります。

これは、双方向のデータバインディングも実現できることを意味します。

#AngularJs
var Scope = function( ) {
    this.$$watchers = [];
};

Scope.prototype.$watch = function( wExp, listener ) {
    this.$$watchers.push( {
        wExp: wExp,
        listener: listener || function() {}
    } );
};

Scope.prototype.$digest = function( ) {
    var dirty;

    do {
            dirtyChecking = false;

            for( var i = 0; i < this.$$watchers.length; i++ ) {
                var newVal = this.$$watchers[i].wExp(),
                    oldVal = this.$$watchers[i].last;

                if( oldVal !== newVal ) {
                    this.$$watchers[i].listener(newVal, oldVal);

                    dirtyChecking = true;

                    this.$$watchers[i].last = newVal;
                }
            }
    } while(dirtyChecking);
};


var $scope = new Scope();

$scope.newName = 'Subhan';

var element = document.querySelectorAll('input');

element[0].onkeyup = function() {
    $scope.newName = element[0].value;

    $scope.$digest();
};

$scope.$watch(function(){
    return $scope.newName;
}, function( newVal, oldVal ) {
    console.log('Value updated In Input - The new value is ' + newVal);

    element[0].value = $scope.newName;
} );

var updateScopeValue = function updateScopeValue( ) {
    $scope.name = 'Butt';
    $scope.$digest();
};

この手法を使用すると、$scope.newName に示されているように、入力の値を簡単に更新できます。updateScopeValue を呼び出すこともでき、入力の値はそれを示します。

Rana Hasnain Khan avatar Rana Hasnain Khan avatar

Rana is a computer science graduate passionate about helping people to build and diagnose scalable web application problems and problems developers face across the full-stack.

LinkedIn