This article illustrates the principle of ANGULARJS data binding. Share to everyone for your reference, specific as follows:
Note
This article is written primarily for beginners, and for those who are just beginning to contact angular and want to know how the data help is working. If you already know more about angular, it is highly recommended that you read the source code directly.
Angular users want to know how data binding is implemented. You may see all kinds of words: $watch, $apply, $digest, dirty-checking ... What are they? How do they work? Here I would like to answer these questions, in fact they have been in the official document has been answered, but I still want to combine them, but I just use a simple way to explain, if you want to understand the technical details, view the source code.
Let's start from the beginning.
Browser event loops and Angular.js extensions
Our browsers have been waiting for events, such as user interaction. If you click a button or enter something in the input box, the callback function of the event executes in the JavaScript interpreter, and then you can do any DOM operation, and the browser will change the DOM accordingly when the callback function is finished. Angular expands this event loop to generate an execution environment that sometimes becomes the angular context (remember, this is an important concept), and we need to explain more concepts to explain what contexts are and how it works.
$watch Queue ($watch list)
Every time you bind something to your UI, you insert a $watch into the $watch team column. Imagine that $watch is something that can detect changes in the model it watches. For example, you have the following code
Index.html
User: <input type= "text" ng-model= "user"/> Password: <input type= "Password"
ng-model= "pass"/>
Here we have a $scope.user, he's bound to the first input box, and a $scope.pass, it's bound to the second input box, and then we add two $watch to the $watch list.
Then look at the following example:
Controllers.js
App.controller (' Mainctrl ', function ($scope) {
$scope. foo = "Foo";
$scope. World = ' world ';
};
Index.html
Here, even if we add two things to the $scope, only one is bound to the UI, so only one $watch is generated here.
Look at the following example:
Controllers.js
App.controller (' Mainctrl ', function ($scope) {
$scope. People = [...];
});
Index.html
<ul>
<li ng-repeat= "person in people" >
{{person.name}}-{{person.age}}
</li>
</ul>
How many $watch have been generated here? Each person has two (one name, one age), and then there is one, so 10 ng-repeat are (2 * 10) +1, which means 21 $watch. Therefore, each data bound to the UI will generate a $watch. Yes, so when did this write $watch? When our template is loaded, that is, in the linking phase (angular is divided into compile and linking phases---translator), the angular interpreter looks for each directive and then generates each $watch that is needed. Sounds good, huh, but, then?
$digest Cycle
Remember the extended event loop I mentioned earlier? $digest loop triggers when the browser receives an event that can be processed by the angular context. This loop is composed of two smaller loops. One handles the Evalasync queue and the other deals with the $watch queue, which is also the subject of this blog post. What does this deal with? $digest will traverse our $watch and ask:
Hey, $watch, what's your value?
is 9.
Okay, did it ever change?
No, sir.
(this variable has not changed, the next one)
What about you, what's your value?
Report, is foo.
Did you just change it?
Changed, just bar.
(Well, we have the DOM needed to be updated)
Continue to ask if you know $watch queues have been checked.
This is called the dirty-checking. Since all the $watch have been checked, it is necessary to ask: Has $watch been updated? If there is at least one update, the loop will trigger again until all the $watch are unchanged. This will ensure that each model will no longer change. Remember that if the loop is more than 10 times, it throws an exception to prevent an infinite loop. When the $digest loop ends, the DOM changes accordingly.
For example: Controllers.js
App.controller (' Mainctrl ', function () {
$scope. Name = "Foo";
$scope. Changefoo = function () {
$scope. Name = "Bar";
}
);
Index.html
{{Name}}
<button ng-click= "Changefoo ()" >change the name</button>
Here we have a $watch because the Ng-click does not generate $watch (the function does not change).
We press the button.
The browser receives an event and goes into the angular context (which explains why later).
$digest loop begins execution, querying each $watch for changes.
Because monitoring $scope.name $watch reports changes, it forces a $digest loop to be executed again.
The new $digest loop does not detect changes.
The browser takes back control and updates the DOM for the corresponding portion of the $scope.name new value.
It's important here (and a lot of people's very sore places) that every event entering the angular context performs a $digest loop, which means that every time we enter a letter loop, we check all the $watch of the entire page.
Through $apply to enter the angular context
Who decides what events enter the angular context and which do not enter? $apply!
If you call $apply when the event is triggered, it goes into the angular context and does not enter if there is no call. Now you may ask: I did not call $apply in the example, why? Angular to do it! So when you click on an element with Ng-click, the time is encapsulated into a $apply call. If you have a ng-model= "foo" input box, and then you knock an F, the event will call $apply ("foo = ' F ';").
When does angular not automatically $apply for us?
This is a common sore spot for angular novices. Why doesn't my jquery update what I'm bound to? Since jquery did not invoke $apply, the event did not enter the angular context, $digest loop was never executed.
Let's take a look at an interesting example:
Let's say we have the following directive and controller.
App.js
App.directive (' clickable ', function () {return
{
restrict: "E",
scope: {
foo: ' = ',
bar: ' = '
} ,
Template: ' <ul style= ' Background-color:lightblue ' ><li>{{foo}}</li><li>{{bar}}</ Li></ul> ',
link:function (scope, element, attrs) {
element.bind (' click ', Function () {
scope.foo++;
scope.bar++;
});
}}; App.controller (' Mainctrl ', function ($scope) {
$scope. foo = 0;
$scope. bar = 0;
});
It binds foo and bar from controller to a list, and each time the element is clicked, Foo and bar will increase by 1.
So what happens when we click on elements? Can we see the update? The answer is in the negative. Because the Click event is a common event that is not encapsulated into the $apply, does that mean we lose our count? No
The real result: the $scope did change, but there was no forced $digest loop, and the $watch that watched Foo and bar were not executed. That is to say, if we perform a $apply on our own, then these $watch will see these changes and then update the DOM as needed.
Try it: http://jsbin.com/opimat/2/
If we click on this directive (blue area), we don't see any changes, but when we click on the button, the number of clicks is updated. As we said just now, we don't trigger $digest loops when we click on this directive, but when the button is clicked, Ng-click calls $apply and then executes the $digest loop, so all the $watch are checked, Of course, including our Foo and bar $watch.
Now you think that's not what you want, what you want is to update the number of clicks when you click on the blue area. Quite simply, perform a $apply on it:
Element.bind (' click ', Function () {
scope.foo++;
scope.bar++;
Scope $apply ();
});
$apply is a function of our $scope (or scope in the link function in Direcvie), calling it will force a $digest loop (unless a loop is currently executing, in which case an exception is thrown, which we do not need to execute $ Apply for the logo).
Try it: Http://jsbin.com/opimat/3/edit
It works! But there's a better way to use $apply:
Element.bind (' click ', Function () {
scope. $apply (function () {
scope.foo++;
scope.bar++;
});
What's different? The difference is that in the first version, we are updating the data outside the angular context, and if there is an error, angular never know. Obviously, there's no big mistake in this example of a little toy, but imagine if we have an alert box that shows the error to the user, and then we have a third party library that makes a network call and then fails, and if we don't encapsulate it in $apply, angular never know it's failed. , the alert box will never bounce out.
So if you want to use a jquery plugin and you want to perform a $digest loop to update your DOM, make sure you call $apply.
Sometimes I want to say that some people "feel bad" when they have to call $apply because they think they've done something wrong. Not really, angular is not a magician, he doesn't know. Third-party libraries want to update bound data.
Use $watch to monitor your own stuff.
You already know that any bindings we set have a $watch of its own, update the DOM when needed, but what if we want to customize our own watches? Simple
Take a look at an example:
App.js
App.controller (' Mainctrl ', function ($scope) {
$scope. Name = "Angular";
$scope. Updated =-1;
$scope. $watch (' name ', function () {
$scope. updated++;
});
Index.html
<body ng-controller= "Mainctrl" >
<input ng-model= "name"/>
Name updated: {{updated}} times.
</body>
This is the way we create a new $watch. The first argument is a string or function, here is just a string, the name of the variable we want to monitor, here, $scope. Name (Note that we only need to use name). The second parameter is executed when $watch says that the expression I am monitoring has changed. The first thing we need to know is that when controller executes to this $watch, it executes immediately, so we set updated to-1.
Try it: Http://jsbin.com/ucaxan/1/edit
Example 2:
App.js
App.controller (' Mainctrl ', function ($scope) {
$scope. Name = "Angular";
$scope. Updated = 0;
$scope. $watch (' name ', function (NewValue, oldValue) {
if (newvalue = = OldValue) {return;}//AKA A-run
$sco pe.updated++;
});
Index.html
<body ng-controller= "Mainctrl" >
<input ng-model= "name"/>
Name updated: {{updated}} times.
</body>
The second parameter of watch accepts two parameters, new values, and old values. We can use them to skip the first execution. Usually you don't have to skip the first execution, but in this case you need it. Be flexible, boy.
Example 3:
App.js
App.controller (' Mainctrl ', function ($scope) {
$scope. user = {name: "Fox"};
$scope. Updated = 0;
$scope. $watch (' user ', function (NewValue, oldValue) {
if (newvalue = = OldValue) {return;}
$scope. updated++;
});
Index.html
<body ng-controller= "Mainctrl" >
<input ng-model= "User.Name"/>
Name updated: {{Updated}} times .
</body>
We want to monitor any changes in the $scope.user object, just as before. Here is an object instead of the previous string.
Try it: Http://jsbin.com/ucaxan/3/edit
Uh? No use, why? Because $watch defaults to compare two objects for the same reference, in examples 1 and 2, each change $scope.name creates a new base variable, so $watch executes because the reference to the variable has changed. In the above example, we are monitoring $scope.user, when we change the $scope.user.name, the reference to $scope.user will not change, we just create a new $scope.user.name each time, but $ Scope.user is always the same.
Example 4:
App.js
App.controller (' Mainctrl ', function ($scope) {
$scope. user = {name: "Fox"};
$scope. Updated = 0;
$scope. $watch (' user ', function (NewValue, oldValue) {
if (newvalue = = OldValue) {return;}
$scope. updated++;
}, True);
Index.html
<body ng-controller= "Mainctrl" >
<input ng-model= "User.Name"/>
Name updated: {{Updated}} times .
</body>
Try it: Http://jsbin.com/ucaxan/4/edit
Now it works! Because we added a third argument to $watch, it's a bool type argument, which means we're comparing the object's value to the reference. Because $scope.user changes when we update $scope.user.name, it can be triggered correctly.
There are a lot of tips&tricks about $watch, but these are the basics.
Summarize
Well, I hope you've learned how data binding works in angular. I guess your first impression is that dirty-checking is slow, well, it's really wrong. It's as fast as lightning. But yes, if you have 2000-3000 watch in a template, it starts to slow down. But I think if you reach this order of magnitude, you can find a user experience expert to consult.
In any case, with the advent of ECMASCRIPT6, we will have object.observe that will greatly improve the speed of the $digest cycle in angular future versions. At the same time, future articles will also involve some tips&tricks.
On the other hand, this topic is not easy, if you find that I left something important or something completely wrong, please correct (the original is on the GitHub PR or report issue)
I hope this article will help you to Angularjs program design.