Learn more about the two way binding mechanism for data in AngularJS

  • 2021-01-19 21:58:12
  • OfStack

Angular JS (Angular.JS) is a set of frameworks, templates, and data binding and enrichment UI components for developing Web pages. It supports the entire development process and provides the architecture for web applications without the need for manual DOM operations. ES9en is small, only 60 ES10en, compatible with major browsers, and works well with ES11en. Two-way data binding is probably the coolest and most useful feature of AngularJS, and it really illustrates the principle of MVC.

How AngularJS works is that the HTML template is parsed into DOM by the browser, and the DOM structure becomes input to the AngularJS compiler. AngularJS will traverse the DOM template to generate the corresponding NG directives, all of which are responsible for setting the data binding for view(ng-model in HTML). Therefore, the NG framework does not come into play until DOM has been loaded.

In html:


<body ng-app="ngApp">
 <div ng-controller="ngCtl">
  <label ng-model="myLabel"></label>
  <input type="text" ng-model="myInput" />
  <button ng-model="myButton" ng-click="btnClicked"></button>
 </div>
</body>

In js:


// angular app
var app = angular.module("ngApp", [], function(){
 console.log("ng-app : ngApp");
});
// angular controller
app.controller("ngCtl", [ '$scope', function($scope){
 console.log("ng-controller : ngCtl");
 $scope.myLabel = "text for label";
 $scope.myInput = "text for input";
 $scope.btnClicked = function() {
  console.log("Label is " + $scope.myLabel);
 }
}]);

As mentioned above, in html we define 1 app for angular and 1 controller for angular, then the controller will correspond to 1 scope (we can use the $scope prefix to specify properties and methods in the scope, etc.). The value or operation of the HTML tag in the scope of the ngCtl can be bound to the properties and methods in the js using the $scope method.

In this way, two-way data binding of NG is realized: that is, the view presented in HTML is identical to the data in AngularJS. Modify its 1, and the corresponding other end will change accordingly.

We only care about the style of the HTML tag and the properties and methods that are bound in the angular and controller scopes of js. That's all. A lot of the complicated DOM operations have been left out.

This idea, in fact, with jQuery DOM query and operation is completely different, so there are a lot of people suggest that when using AngularJS, do not mix the use of jQuery. Of course, each has its own advantages and disadvantages, and which one to use depends on your choice.

app in NG is equivalent to a module module. Multiple controller can be defined in each app. Each controller will have its own scope space and will not interfere with each other.

How does binding data work
A beginner of AngularJS might step into a pit like this, assuming a command:


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

app.directive("myclick", function() {
  return function (scope, element, attr) {
    element.on("click", function() {
      scope.counter++;
    });
  };
});

app.controller("CounterCtrl", function($scope) {
  $scope.counter = 0;
});
<body ng-app="test">
  <div ng-controller="CounterCtrl">
    <button myclick>increase</button>
    <span ng-bind="counter"></span>
  </div>
</body>

At this point, click the button and the number on the screen will not increase. Many people will be confused because he looks at the debugger and finds that the data has indeed been increased. Isn't ES85en a two-way binding? Why does the data change and the interface doesn't refresh?

Try the scope. counter + +; digest(); scope.digest (); AngularJS.digest (); Let's see if that's all right?

Why do you want to do that, and under what circumstances do you want to do that? We found that in the first example there is no digest, and if you write digest, it will throw an exception saying that you are working on some other digest. What's going on?

Let's start by thinking, what if we wanted to implement this feature ourselves without ES99en?


<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>two-way binding</title>
  </head>
  <body onload="init()">
    <button ng-click="inc">
      increase 1
    </button>
    <button ng-click="inc2">
      increase 2
    </button>
    <span style="color:red" ng-bind="counter"></span>
    <span style="color:blue" ng-bind="counter"></span>
    <span style="color:green" ng-bind="counter"></span>

    <script type="text/javascript">
      /*  The data model area starts  */
      var counter = 0;

      function inc() {
        counter++;
      }

      function inc2() {
        counter+=2;
      }
      /*  End of the data model area  */

      /*  The binding relationship zone starts  */
      function init() {
        bind();
      }

      function bind() {
        var list = document.querySelectorAll("[ng-click]");
        for (var i=0; i<list.length; i++) {
          list[i].onclick = (function(index) {
            return function() {
              window[list[index].getAttribute("ng-click")]();
              apply();
            };
          })(i);
        }
      }

      function apply() {
        var list = document.querySelectorAll("[ng-bind='counter']");
        for (var i=0; i<list.length; i++) {
          list[i].innerHTML = counter;
        }
      }
      /*  The binding relationship zone ends  */
    </script>
  </body>
</html>

As you can see, in a simple example like this, we've done some two-way binding. From two button clicks to data changes, this is easy to understand, but instead of using the onclick method directly, we created an ng-click, and then bound the corresponding function of ng-click into the event handler of onclick. Why is that? Because the data has changed but not yet populated the interface, we need to do something additional here.

On the other hand, when the data changes, the changes need to be applied to the interface, namely the three span. However, since Angular uses dirty detection, this means that when you change the data, you will have to do something to trigger the dirty detection and then apply it to the DOM element corresponding to the data. The question is, how do you trigger dirty detection? When is it triggered?

As we know, some frameworks based on setter can reassign binding variables on DOM elements when setting values to data. The dirty detection mechanism does not have this stage, it does not have any way to be notified immediately after the data changes, so it has to manually call apply() from each event entry to apply the data changes to the interface. In a real Angular implementation, dirty detection is performed here to determine that the data has changed before setting the interface values.

Therefore, we encapsulate the actual click in ng-click, the most important purpose is to append apply() to the interface after the data changes.

Why, then, in ng - click call $digest words, complains? Because Angular design, with 1 time only allowed one run $digest ng - click this built-in commands have triggered a $digest, current haven't finished, so I made a mistake.

$$apply digest and
In Angular, there are two functions: $apply and $digest. We just used $digest to apply this data to the interface. $apply = $digest; $apply = $digest; $apply = $digest;

The most immediate difference is that $apply can take arguments; it can take a function and then call that function after the data is applied. So, 1 in the integration of non-Angular framework code, you can write code in this call.


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

app.directive("myclick", function() {
  return function (scope, element, attr) {
    element.on("click", function() {
      scope.counter++;
      scope.$apply(function() {
        scope.counter++;
      });
    });
  };
});

app.controller("CounterCtrl", function($scope) {
  $scope.counter = 0;
});

Is there any other difference besides that?

In a simple data model, there is no essential difference between the two, but when there is a hierarchy, it is different. Given that there are two levels of scope, we can call the two functions on the parent scope or on the child scope, at which point we can see the difference.

For $digest, there is a difference between calling on the parent scope and the child scope, but for $apply, the two are identical. Let's construct a special example:


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

app.directive("increasea", function() {
  return function (scope, element, attr) {
    element.on("click", function() {
      scope.a++;
      scope.$digest();
    });
  };
});

app.directive("increaseb", function() {
  return function (scope, element, attr) {
    element.on("click", function() {
      scope.b++;
      scope.$digest();  // The switch to $apply Can be 
    });
  };
});

app.controller("OuterCtrl", ["$scope", function($scope) {
  $scope.a = 1;

  $scope.$watch("a", function(newVal) {
    console.log("a:" + newVal);
  });

  $scope.$on("test", function(evt) {
    $scope.a++;
  });
}]);

app.controller("InnerCtrl", ["$scope", function($scope) {
  $scope.b = 2;

  $scope.$watch("b", function(newVal) {
    console.log("b:" + newVal);
    $scope.$emit("test", newVal);
  });
}]);
<div ng-app="test">
  <div ng-controller="OuterCtrl">
    <div ng-controller="InnerCtrl">
      <button increaseb>increase b</button>
      <span ng-bind="b"></span>
    </div>
    <button increasea>increase a</button>
    <span ng-bind="a"></span>
  </div>
</div>

At this time, we can see the difference, in the increase b button click, at this time, a and b values have changed, but the interface a did not update, until click 1 increase a, this time just on the a accumulation will be 1 update. How to solve this problem? Simply change $digest to $apply in the implementation of the increaseb directive.

When $digest is called, only monitoring on the current scope and its child scopes is triggered, but when $apply is called, all monitoring on the scope tree is triggered.

Therefore, from a performance perspective, if you can determine the scope of your data change, you should call $digest as much as possible, and only use $apply when you can't know exactly the scope of your data change, violently traversal the entire scope tree and call all monitors in it.

From another point of view, we can also see why it is recommended to put it in $apply when calling external frameworks, because this is the only place where all data changes are applied, and if $digest is used, it is possible to temporarily lose data changes.

The advantages and disadvantages of dirty detection
Many people disdain the dirty detection mechanism of Angular, and recommend the observation mechanism based on setter and getter. In my opinion, this is just a different implementation of the same thing, and no one is completely superior to the other. Both have their own advantages and disadvantages.

As you all know, in a loop batch adding DOM elements, will be recommended DocumentFragment, why, because if every time for DOM produces change, it will modify DOM tree structure, affect the performance, if we can first DOM structure is created in the document fragments, and then the whole added to the master document, the DOM tree will change once completed, performance will improve a lot.

Similarly, in the Angular framework, the following scenario is considered:


function TestCtrl($scope) {
  $scope.numOfCheckedItems = 0;

  var list = [];

  for (var i=0; i<10000; i++) {
    list.push({
      index: i,
      checked: false
    });
  }

  $scope.list = list;

  $scope.toggleChecked = function(flag) {
    for (var i=0; i<list.length; i++) {
      list[i].checked = flag;
      $scope.numOfCheckedItems++;
    }
  };
}

What happens if some text on the interface is bound to this numOfCheckedItems? With the dirty detection mechanism, this process is stress-free, all data changes are made in one go and then applied to the interface as a whole. At this point, the setter-based mechanism is miserable, unless it also delays batch operations to a single update, as Angular does, the performance will be even worse.

Therefore, the two different monitoring methods have their own advantages and disadvantages, and the best approach is to understand the differences in their use, take into account the differences in their performance, and avoid the most likely performance bottlenecks in different business scenarios.


Related articles: