UPDATE: This post is meant for beginners,for those that just started to learn Angular and want to know how data-binding works. If you already know how to use Angular properly,I highly suggest you go to the source code instead.
Angular users want to know how data-binding works. There is a lot of vocabulary around this:$watch@H_404_7@,
$apply@H_404_7@,monospace; font-size:0.8em; vertical-align:baseline; display:inline-block">$digest@H_404_7@,monospace; font-size:0.8em; vertical-align:baseline; display:inline-block">dirty-checking@H_404_7@… What are they and how do they work? Here I want to address all those questions,which are well addressed in the documentation,but I want to glue some pieces together to address everything in here,but keep in mind that I want to do that in a simple way. For more technical issues,check the source.
Let’s start from the beginning.
The browser events-loop and the Angular.js extension
Our browser is waiting for events,for example the user interactions. If you click on a button or write into an input,the event’s callback will run inside Javascript and there you can do any DOM manipulation,so when the callback is done,the browser will make the appropiate changes in the DOM.
Angular extends this events-loop creating something calledangular context@H_404_7@(remember this,it is an important concept). To explain what this context is and how it works we will need to explain more concepts.
The $watch list
Every time you bind something in the UI you insert a$watch@H_404_7@in a
$watch list@H_404_7@. Imagine the
$watch@H_404_7@as something that is able to detect changes in the model it is watching (bear with me,this will be clear soon). Imagine you have this:
Here we have$scope.user@H_404_7@,which is bound to the first input,and we have
$scope.pass@H_404_7@,which is bound to the second one. Doing this we add two
$watch@H_404_7@to the
$watch list@H_404_7@.
app.controller('MainCtrl', function($scope) { $scope.foo = "Foo"; world = "World"; });
Hello,{{ World }}
Here,even though we have two things attached to the$scope@H_404_7@,only one is bound. So in this case we only created one
$watch@H_404_7@.
people = [...]; });
<ul> <li ng-repeat="person in people"> {{person.name}} - {{person.age}} </li> </ul>
How many$watch@H_404_7@are created here? Two for each person (for name and age) in people plus one for the
ng-repeat@H_404_7@. If we have 10 people in the list it will be
(2 * 10) + 1@H_404_7@,AKA
21@H_404_7@
$watch@H_404_7@.
So,everything that is bound in our UI using directives creates a$watch@H_404_7@. Right,but when are those
$watch@H_404_7@created?
When our template is loaded,AKA in thelinking phase@H_404_7@,the compiler will look for every directive and creates all the
$watch@H_404_7@that are needed. This sounds good,but… now what?
$digest loop
Remember the extendedevent-loop@H_404_7@I talked about? When the browser receives an event that can be managed by the
angular context@H_404_7@the
$digest@H_404_7@loop will be fired. This loop is made from two smaller loops. One processes the
$evalAsync@H_404_7@queue and the other one processes the
$watch@H_404_7@list,which is the subject of this article.
What is that process about? The$digest@H_404_7@will loop through the list of
$watch@H_404_7@that we have,asking this:
- Hey
- It is
9@H_404_7@
- It is
- No,sir.
- Yes,it was
Bar@H_404_7@.
$watch@H_404_7@has been checked.
This is thedirty-checking@H_404_7@. Now that all the
$watch@H_404_7@have been checked there is something else to ask: Is there any
$watch@H_404_7@that has been updated? If there is at least one of them that has changed,the loop will fire again until all of the
$watch@H_404_7@report no changes. This is to ensure that every model is clean. Have in mind that if the loop runs more than 10 times,it will throw an exception to prevent infinite loops.
When the$digest loop@H_404_7@finishes,the DOM makes the changes.
Example:
controllers.jsfunction() { name = "Foo"; changeFoo = "Bar"; } });
Here we have only one$watch@H_404_7@because ng-click doesn’t create any watches (the function is not going to change :P).
- We press the button.
- The browser receives an event which will enter the
angular context@H_404_7@(I will explain why,later in this article).
- The
$digest loop@H_404_7@will run and will ask every
$watch@H_404_7@for changes.
- Since the
$watch@H_404_7@which was watching for changes in
$scope.name@H_404_7@reports a change,if will force another $digest loop.
- The new loop reports nothing.
- The browser gets the control back and it will update the DOM reflecting the new value of
$scope.name@H_404_7@
The important thing here (which is seen as a pain-point by many people) is that EVERY event that enters theangular context@H_404_7@will run a
$digest loop@H_404_7@. That means that every time we write a letter in an input,the loop will run checking every
$watch@H_404_7@in this page.
Entering the angular context with $apply
What says which events enter the angular context and which ones do not?$apply@H_404_7@
If you call$apply@H_404_7@when an event is fired,it will go through the
angular-context@H_404_7@,but if you don’t call it,it will run outside it. It is as easy as that. So you may now ask… That last example does work and I haven’t called
ng-click@H_404_7@,the event will be wrapped inside an
$apply@H_404_7@call. If you have an input with
ng-model="foo"@H_404_7@and you write an
f@H_404_7@,the event will be called like this:
$apply("foo = 'f';")@H_404_7@,in other words,wrapped in an
$apply@H_404_7@call.
When angular doesn’t use $apply for us
This is the common pain-point for newcomers to Angular. Why is my jQuery not updating my bindings? Because jQuery doesn’t call$apply@H_404_7@and then the events never enter the
angular context@H_404_7@and then the
$digest loop@H_404_7@is never fired.
Let’s see an interesting example:
Imagine we have the following directive and controller:
app.jsdirective('clickable',210)!important">function() { return { restrict: "E", scope: { foo: '=',0)!important">bar: '=' },0)!important">template: '<ul style="background-color: lightblue"><li>{{foo}}</li><li>{{bar}}</li></ul>',0)!important">link: scope, element,0)!important">attrs) { element.bind('click',0)!important">scope.foo++; bar++; }); } } }); foo = 0; bar = 0; });
It bindsfoo@H_404_7@and
bar@H_404_7@from the controller to show them in a list,then every time we click on the element,both
bar@H_404_7@values are incremented by one.
What will happen if we click on the element? Are we going to see the updates? The answer is no. No,because theclick@H_404_7@event is a common event that is not wrapped into an
$apply@H_404_7@call. So that means that we are going to lose our count? No.
What is happening is that the$scope@H_404_7@is indeed changing but since that is not forcing a
$digest loop@H_404_7@,the
$watch@H_404_7@for
foo@H_404_7@and the one for
bar@H_404_7@are not running,so they are not aware of the changes. This also means that if we do something else that does run an
$watch@H_404_7@we have will see that they have changed and then update the DOM as needed.
Try it
If we click on the directive (the blue zone) we won’t see any changes,but if we click on the button to update the string next to it,we suddenly see how many times we clicked on the directive. Just what I said,the clicks on the directive won’t trigger any$digest loop@H_404_7@but when the button is clicked on,monospace; font-size:0.8em; vertical-align:baseline; display:inline-block">ng-click@H_404_7@will call
$apply@H_404_7@and it will run the
$watch@H_404_7@we have are going to be checked for changes,and that includes the one for
bar@H_404_7@.
Now you are thinking that this is not what you want,you want to update the bindings as soon as you click on the directive. That is easy,we just need to call$apply@H_404_7@like this:
bar++; $apply(); });
$apply@H_404_7@is a function of our
$scope@H_404_7@(or
scope@H_404_7@inside a directive’s link function) so calling it will force a
$digest loop@H_404_7@(except if there is a loop in course,in that case it will throw an exception,which is a sign that we don’t need to call
$apply@H_404_7@there).
Try it
It works! But there is a better way for using$apply@H_404_7@:
What’s the difference? The difference is that in the first version,we are updating the values outside theangular context@H_404_7@so if that throws an error,Angular will never know. ObvIoUsly in this tiny toy example it won’t make much difference,but imagine that we have an alert Box to show errors to our users and we have a 3rd party library that does a network call and it fails. If we don’t wrap it inside an
Box won’t be there.
So if you want to use a jQuery plugin,be sure you call$apply@H_404_7@if you need to run a
$digest loop@H_404_7@to update your DOM.
Something I want to add is that some people “feel bad” having to call$apply@H_404_7@because they think that they are doing something wrong. That is not true. It is just Angular that is not a magician and it doesn’t know when a 3rd party library wants to update the bindings.
Using $watch for our own stuff
You already know that every binding we set has its own$watch@H_404_7@to update the DOM when is needed,but what if we want our own watches for our purposes? Easy.
Let’s see some examples:
app.js"Angular"; updated = -1; $watch('name',0)!important">updated++; }); });
That is how we create a new I initialized the Example 2: The second parameter of Example 3: We want to Uhm? It doesn’t work. Why? Because the That is obvIoUsly not the desired case in this example. Example 4: Now it is working! How? We added a third parameter to the There are more tips & tricks with Well,I hope you have learnt how data-binding works in Angular. I guess that your first impression is that this Anyway,in a future version of Angular and with the release of EcmaScript 6,we will have On the other hand,this topic is not easy and if you find that I missed something important or there is anything completely wrong,please fill an issue at Github or write a pull request :).$watch@H_404_7@. The first parameter can be a string or a function. In this case it is just a string with the name of what we want to
$scope.name@H_404_7@(notice how we just need to use
name@H_404_7@). The second parameter is what is going to happen when
$watch@H_404_7@says that our watched expression has changed. The first thing we have to know is that when the controller is executed and finds the
Try it
$scope.updated@H_404_7@to
-1@H_404_7@because as I said,monospace; font-size:0.8em; vertical-align:baseline; display:inline-block">$watch@H_404_7@will run once when it is processed and it will put the
$scope.updated@H_404_7@to 0.
updated = 0; newValue,0)!important">oldValue) { if (newValue === oldValue) { return; } // AKA first run </body>
$watch@H_404_7@receives two parameters. The new value and the old value. We can use them to skip the first run that every
$watch@H_404_7@does. Normally you don’t need to skip the first run,but in the rare cases where you need it (like this one),this trick comes in handy.
$watch@H_404_7@any changes in our
$scope.user@H_404_7@object. Same as before but using an object instead of a primitive.
Try it
$watch@H_404_7@by default compares the reference of the objects. In example 1 and 2,every time we modify
$scope.name@H_404_7@it will create a new primitive,so the
$watch@H_404_7@will fire because the reference of the object is new and that is our change. In this new case,since we are watching
$scope.user@H_404_7@and then we are changing
$scope.user.name@H_404_7@,the reference of
$scope.user@H_404_7@is never changing because we are creating a new
$scope.user.name@H_404_7@every time we change the input,but the
$scope.user@H_404_7@will be always the same.
Try it
$watch@H_404_7@which is a
bool@H_404_7@to indicate that we want to compare the value of the objects instead of the reference. And since the value of
$scope.user@H_404_7@is changing when we update the
$scope.user.name@H_404_7@the
$watch@H_404_7@will fire appropriately.
$watch@H_404_7@but these are the basics.
Conclusion
dirty-checking@H_404_7@is slow; well,that is not true. It is fast as lightning. But yes,if you have something like 2000-3000
$watch@H_404_7@in a template,it will become laggy. But I think that if you reach that,it would be time to ask an UX expert :P.
Object.observe@H_404_7@which will improve the
$digest loop@H_404_7@a lot. Meanwhile there are some tips & tricks that I am going to cover in a future article.