AngularJS의 더러운 검사

Rana Hasnain Khan 2022년5월31일
AngularJS의 더러운 검사

AngularJs에서 더티 검사를 수행하는 방법을 소개합니다.

AngularJS에서 Dirty Checking 구현

AngularJS는 양방향 데이터 바인딩을 위해 $scope 변수에 대해 더티 검사를 수행합니다. Ember.js는 프로그래밍 방식으로 setter와 getter를 수정하여 양방향 데이터 바인딩을 수행하지만 Angular에서는 더티 검사를 통해 사용 가능한 변수를 찾을 수 있습니다.

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

$scope.$watch 함수는 변수가 변경된 시점을 보고 싶을 때 사용합니다. 이 함수를 적용하려면 세 가지 인수를 선택해야 합니다. 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를 설정할 때 하나의 인수만 사용하면 업데이트 여부에 관계없이 선택한 기능이 적용됩니다. ng-model에 있는 기능은 모델과 보기가 동기화되어 있는지 여부를 검사합니다.

동기화되지 않은 경우 함수는 모델에 새 값을 제공하고 업데이트합니다. 이 함수를 사용하면 $digest에서 이 함수를 실행할 때 새 값을 반환하여 새 값이 무엇인지 알 수 있습니다.

리스너가 해고되지 않는 이유

우리가 설명한 것과 유사한 기능에서 $scope.$watch 기능을 등록 해제한 예제의 요점으로 돌아가 보겠습니다. 이제 $scope.name을 업데이트했는데도 업데이트에 대한 알림을 받지 못하는 이유를 알 수 있습니다.

아시다시피 $scope.$apply는 각 지시문 컨트롤러 기능에서 Angular에 의해 실행됩니다. $scope.$apply 기능은 지시어의 컨트롤러 기능을 추정했을 때 한 가지 조건에서만 $digest를 실행합니다.

이는 $scope.$watch 함수가 실행되기 전에 등록을 해제했기 때문에 호출될 가능성이 최소 또는 전혀 없음을 의미합니다.

AngularJS의 $digest 함수

$scope.$apply에 의해 $rootScope에서 이 기능을 호출할 수 있습니다. $rootScope에서 다이제스트 주기를 실행할 수 있습니다. 그러면 범위를 전달하고 대신 다이제스트 주기를 실행합니다.

다이제스트 주기는 $$watchers 변수에 있는 모든 wExp 기능을 실행합니다. 마지막으로 알려진 값과 비교하여 확인합니다.

결과가 음수이면 리스너가 실행됩니다.

다이제스트 주기에서 실행될 때 두 번 반복됩니다. 한 번, 그것은 감시자들을 가로질러 반복될 것이고, 그 다음 주기가 더 이상 더러운 상태가 아닐 때까지 다시 반복될 것입니다.

wExp와 마지막으로 알려진 값이 동일하지 않은 경우 주기가 더티라고 합니다. 일반적으로 이 주기는 한 번 실행되고 10번 이상 실행되면 오류가 발생합니다.

Angular는 모델 값이 변경될 가능성이 있는 모든 항목에서 $scope.$apply를 실행할 수 있습니다.

$scope.$apply();를 실행해야 합니다. Angular 외부에서 $scope를 업데이트했을 때. 이렇게 하면 범위가 업데이트되었음을 ​​Angular에 알립니다.

기본 버전의 더티 검사를 설계해 보겠습니다.

저장하려는 모든 데이터는 범위 기능에 있습니다. $digest$watch를 복제하기 위해 함수의 개체를 확장합니다.

Scope와 관련된 기능을 평가할 필요가 없기 때문에 $apply를 사용하지 않습니다. $digest를 직접 사용합니다.

스코프는 다음과 같습니다.

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

Scope.prototype.$watch = function( ) {

};

Scope.prototype.$digest = function( ) {

};

$watch가 채택해야 하는 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