$watch How the $apply Runs a $digest

前端之家收集整理的这篇文章主要介绍了$watch How the $apply Runs a $digest前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

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,$apply,monospace; font-size:0.8em; vertical-align:baseline; display:inline-block">$digest,monospace; font-size:0.8em; vertical-align:baseline; display:inline-block">dirty-checking… 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(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$watchin a$watch list. Imagine the$watchas 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:

index.html
1
2
User: <input type="text" ng-model="user" /> Password: "password" "pass" /> 

Here we have$scope.user,which is bound to the first input,and we have$scope.pass,which is bound to the second one. Doing this we add two$watchto the$watch list.

controllers.js
2 3 4
app.controller('MainCtrl', function($scope) {  $scope.foo = "Foo";  world = "World"; }); 
index.html
1
Hello,{{ World }} 

Here,even though we have two things attached to the$scope,only one is bound. So in this case we only created one$watch.

controllers.js
3
people = [...]; }); 
index.html
4 5
<ul>  <li ng-repeat="person in people">  {{person.name}} - {{person.age}}  </li> </ul> 

How many$watchare created here? Two for each person (for name and age) in people plus one for theng-repeat. If we have 10 people in the list it will be(2 * 10) + 1,AKA21$watch.

So,everything that is bound in our UI using directives creates a$watch. Right,but when are those$watchcreated?

When our template is loaded,AKA in thelinking phase,the compiler will look for every directive and creates all the$watchthat are needed. This sounds good,but… now what?

$digest loop

Remember the extendedevent-loopI talked about? When the browser receives an event that can be managed by theangular contextthe$digestloop will be fired. This loop is made from two smaller loops. One processes the$evalAsyncqueue and the other one processes the$watchlist,which is the subject of this article.

What is that process about? The$digestwill loop through the list of$watchthat we have,asking this:

  • Hey
  • It is9
  • Alright,has it changed?
    • No,sir.
  • (nothing happens with this one,so it moves to the next)
  • You,monospace; font-size:0.8em; vertical-align:baseline; display:inline-block">Foo.
  • Has it changed?
    • Yes,it wasBar.
  • (good,we have a DOM to be updated)
  • This continues until every$watchhas been checked.
  • This is thedirty-checking. Now that all the$watchhave been checked there is something else to ask: Is there any$watchthat has been updated? If there is at least one of them that has changed,the loop will fire again until all of the$watchreport 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 loopfinishes,the DOM makes the changes.

    Example:

    controllers.js
    5 6 7
    function() {  name = "Foo";   changeFoo = "Bar";  } }); 
    index.html
    {{ name }} <button ng-click="changeFoo()">Change the name</button>

    Here we have only one$watchbecause 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 theangular context(I will explain why,later in this article).
    • The$digest loopwill run and will ask every$watchfor changes.
    • Since the$watchwhich was watching for changes in$scope.namereports 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

    The important thing here (which is seen as a pain-point by many people) is that EVERY event that enters theangular contextwill run a$digest loop. That means that every time we write a letter in an input,the loop will run checking every$watchin this page.

    Entering the angular context with $apply

    What says which events enter the angular context and which ones do not?$apply

    If you call$applywhen an event is fired,it will go through theangular-context,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 calledng-click,the event will be wrapped inside an$applycall. If you have an input withng-model="foo"and you write anf,the event will be called like this:$apply("foo = 'f';"),in other words,wrapped in an$applycall.

    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$applyand then the events never enter theangular contextand then the$digest loopis never fired.

    Let’s see an interesting example:

    Imagine we have the following directive and controller:

    app.js
    7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
    directive('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 bindsfooandbarfrom the controller to show them in a list,then every time we click on the element,bothbarvalues 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 theclickevent is a common event that is not wrapped into an$applycall. So that means that we are going to lose our count? No.

    What is happening is that the$scopeis indeed changing but since that is not forcing a$digest loop,the$watchforfooand the one forbarare not running,so they are not aware of the changes. This also means that if we do something else that does run an$watchwe have will see that they have changed and then update the DOM as needed.

    Try it