상세 컨텐츠

본문 제목

Anuglar JS 공부 <6> - $apply(), $watch() 변화 감지 그리고 $eval(),$parse()

AngularJS

by oimb 2019. 3. 4. 22:57

본문


1. 변화


angularjs로 개발을 하게 되면 생각대로 값에 대한 변경이 이루어지지 않는 경우가 정말 많다. 그러한 이슈들 중 해결방법은 몇가지 있지만 그거 마저도 굉장히  어렵다.

앞서 소개하고 설명했던 공부들중 몇몇이 이에 해당된다. 이번 공부는 $apply와 $watch가 이에 해당된다.



2. $apply


먼저 $apply에 대해 소개를 시작하겠다.


function $apply(expr) {

  try {

    return $eval(expr);

  } catch (e) {

    $exceptionHandler(e);

  } finally {

    $root.$digest();

  }

}


$apply는 위와같이 구현되어져 있다. 따라서 $eval을 반드시 수행하며 이후 에러가 나지않는다면 $digest를 수행하게 된다.  먼저 $eval을 알아보면 


$eval([expression], [locals]);

Executes the expression on the current scope and returns the result. Any exceptions in the expression are propagated (uncaught). This is useful when evaluating AngularJS expressions.

Parameters

ParamTypeDetails
expression
(optional)
stringfunction()

An AngularJS expression to be executed.

  • string: execute using the rules as defined in expression.
  • function(scope): execute the function with the current scope parameter.
locals
(optional)
object

Local variables object, useful for overriding values in scope.

angularjs API를 보면 이렇게 나와있다. 즉 exp를 실행시켜 그에 대한 결과값을 반환하게 되는데 이때 exp는 string형태 또는 함수 형태이다. 함수 형태는 실행시킨후에 리턴값을 반환하는구나 라고 이해할수 있지만 string은 이해하기 힘들 수 있다. 자 예제를 살펴보자.


<!DOCTYPE html>

<html>

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

<body ng-app="myApp">

    <input type="text" mobi-a="search"/>

    <script>

        var app = angular.module("myApp", []);

        app.directive("mobiA", function() {

            return {

               controller :['$scope',function(scope){

                   scope.search = function(){

                       return '==> hi';

                   }

               }],

               link : function(scope,element,attrs){

                   element.bind('keypress',function(e){

                       if(e.which==13){

                           scope.$apply(function(){

                               var msg = scope.$eval(attrs.mobiA)

                               console.log(msg());

                               console.log("===>>>",e.target.value)

                           })

                           e.preventDefault();

                       }

                   })

               }

           };

       });

   </script>

</body>

</html>


콘솔에 위와 같이 찍히는 결과값을 알수 있다. attrs.mobiA는 "search"를 반환하게 되고 이를 $eval("search")형태로 수행하게 된다. 즉 이는 곧 search함수를 리턴하고 이를

msg가 받은후에 msg()형태로 호출하게 된다.


다시 본론으로 돌아와 $apply는 위와 같이 exp를 $eval로 실행후에 결과값을 리턴하며 $digest를 호출하여 digest loop를 돌게 한다. 이는 공부<1>에서 언급한바 있다.

따라서 위와같이 동적으로 생성된 이벤트에 의해 변경된 모델값을 감지하기 위해서는 $apply를 수행하여 $digest를 수행시켜야만 한다.


마지막 예를 보고 다음으로 넘어가자


<!DOCTYPE html>

<html>

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

<body>

<div ng-app="myApp" ng-controller="myCtrl">

event : <input type="text" ng-model="name" f-d="{{lastName}}"><br>

<br>

Name: {{asd}}

</div>

<script>

var app = angular.module('myApp', []);

app.controller('myCtrl', function($scope) {

$scope.lastName = "Doe";

})

.directive("fD",function(){

return function(scope,element,attrs){

element.bind('keyup',function(){

scope.$apply(function(){

scope.asd = attrs.fD;

})

})

}

})

</script>

</body>

</html>




3. $watch


$watch는 이해하기 쉬울것이다. 이것도 역시 결국은 모델값의 변경을 감지하기 위한것인데 watchList를 수색하여 변경된 모델값을 감지시키는 것이다.


$watch(watchExpression, listener, [objectEquality]);

Registers a listener callback to be executed whenever the watchExpression changes.

  • The watchExpression is called on every call to $digest() and should return the value that will be watched. (watchExpression should not change its value when executed multiple times with the same input because it may be executed multiple times by $digest(). That is, watchExpression should be idempotent.)
  • The listener is called only when the value from the current watchExpression and the previous call to watchExpression are not equal (with the exception of the initial run, see below). Inequality is determined according to reference inequality, strict comparison via the !== Javascript operator, unless objectEquality == true (see next point)
  • When objectEquality == true, inequality of the watchExpression is determined according to the angular.equals function. To save the value of the object for later comparison, the angular.copy function is used. This therefore means that watching complex objects will have adverse memory and performance implications.
  • This should not be used to watch for changes in objects that are (or contain) File objects due to limitations with angular.copy.
  • The watch listener may change the model, which may trigger other listeners to fire. This is achieved by rerunning the watchers until no changes are detected. The rerun iteration limit is 10 to prevent an infinite loop deadlock.

If you want to be notified whenever $digest is called, you can register a watchExpression function with no listener. (Be prepared for multiple calls to your watchExpression because it will execute multiple times in a single $digest cycle if a change is detected.)

After a watcher is registered with the scope, the listener fn is called asynchronously (via $evalAsync) to initialize the watcher. In rare cases, this is undesirable because the listener is called when the result of watchExpression didn't change. To detect this scenario within the listener fn, you can compare the newVal and oldVal. If these two values are identical (===) then the listener was called due to initialization.

Parameters

ParamTypeDetails
watchExpressionfunction()string

Expression that is evaluated on each $digest cycle. A change in the return value triggers a call to the listener.

  • string: Evaluated as expression
  • function(scope): called with current scope as a parameter.
listenerfunction(newVal, oldVal, scope)

Callback called whenever the value of watchExpression changes.

  • newVal contains the current value of the watchExpression
  • oldVal contains the previous value of the watchExpression
  • scope refers to the current scope
objectEquality
(optional)
boolean

Compare for object equality using angular.equals instead of comparing for reference equality.

(default: false)

Returns

function()

Returns a deregistration function for this listener.

위 api를 보면 역시 expression에 대한 설명이 있는데 여기서 exp는 감지되어야 할 대상을 가리키며 감지대상이 변화될때 listener를 호출하게 된다. 이 때 listener는 감지대상의 값 변경에 의한 newValue 와 이전 값 oldValue 그리고 scope를 변수로 받으며 보통 newValue로만 인자를 사용한다.

어느정도 감은 올것이다.

그럼 예제를 보자


<!DOCTYPE html>

<html>

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

<body>

<div ng-app="myApp" ng-controller="myCtrl">

Name: <input type="text" ng-model="name" f-d="test"><br>

<br>

Name: {{asd}}

</div>

<script>

var app = angular.module('myApp', []);

app.controller('myCtrl', function($scope) {

$scope.name = "Doe";

})

.directive("fD",function(){

return function(scope,element,attrs){            

scope.$watch('name',function(newValue){

scope.asd = newValue;

})

}

})

</script>

</body>

</html>


scope.asd의 값변경을 $watch를 사용하지 않고 변경시키려 한다면 아무 변화가 없을 것이다.



4. 번외 $parse


function $parse(expression) {

    return function() {

        return $eval(expression);

    }

}


$parse는 위와같은 구조를 갖는다. 앞서 $eval에 대해 공부했으므로 $parse를 반드시 알고갈 필요가 있어 언급한다.

$parse는 위와같은 구조를 갖고 있기 때문에 다음과 같은 결과를 보여준다.


<!DOCTYPE html>

<html>

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

<body>

<div ng-app="myApp" ng-controller="customersCtrl"> </div>

</body>

<script>

var app = angular.module('myApp', []);

app.controller('customersCtrl', function($scope, $parse) {


$scope.foo = 3;

var parseFn = $parse('foo = 5');

parseFn($scope);

console.log($scope.foo); // returns 5

$scope.foo = 3;

$scope.$eval('foo = 5');

console.log($scope.foo); // returns 5

});

</script>

</html>



위 결과는 $parse와 $scope의 동일한 결과를 보여줌을 알수 있다. 다만 $parse를 언급한 이유는 좀더 유용한 방법이 있기 때문인데


<!DOCTYPE html>

<html>

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

<body>

<div ng-app="myApp" ng-controller="customersCtrl"> </div>

</body>

<script>

var app = angular.module('myApp', []);

app.controller('customersCtrl', function($scope, $parse) {

$scope.text2 =222;


var getter = $parse('text1');

getter.assign($scope,"11111");


var egetter = $scope.$eval('text2');

console.log($scope); //scope.text2 = 222

    

});

</script>

</html>


위와 같이 assign을 통해 scope객체 해당 값을 넣어줄수 있기 때문이다. 이는 디렉티브에서 굉장히 유용하게 사용할 수 있다.

관련글 더보기