How does data binding work in AngularJS?

355,093

Solution 1

AngularJS remembers the value and compares it to a previous value. This is basic dirty-checking. If there is a change in value, then it fires the change event.

The $apply() method, which is what you call when you are transitioning from a non-AngularJS world into an AngularJS world, calls $digest(). A digest is just plain old dirty-checking. It works on all browsers and is totally predictable.

To contrast dirty-checking (AngularJS) vs change listeners (KnockoutJS and Backbone.js): While dirty-checking may seem simple, and even inefficient (I will address that later), it turns out that it is semantically correct all the time, while change listeners have lots of weird corner cases and need things like dependency tracking to make it more semantically correct. KnockoutJS dependency tracking is a clever feature for a problem which AngularJS does not have.

Issues with change listeners:

  • The syntax is atrocious, since browsers do not support it natively. Yes, there are proxies, but they are not semantically correct in all cases, and of course there are no proxies on old browsers. The bottom line is that dirty-checking allows you to do POJO, whereas KnockoutJS and Backbone.js force you to inherit from their classes, and access your data through accessors.
  • Change coalescence. Suppose you have an array of items. Say you want to add items into an array, as you are looping to add, each time you add you are firing events on change, which is rendering the UI. This is very bad for performance. What you want is to update the UI only once, at the end. The change events are too fine-grained.
  • Change listeners fire immediately on a setter, which is a problem, since the change listener can further change data, which fires more change events. This is bad since on your stack you may have several change events happening at once. Suppose you have two arrays which need to be kept in sync for whatever reason. You can only add to one or the other, but each time you add you fire a change event, which now has an inconsistent view of the world. This is a very similar problem to thread locking, which JavaScript avoids since each callback executes exclusively and to completion. Change events break this since setters can have far-reaching consequences which are not intended and non obvious, which creates the thread problem all over again. It turns out that what you want to do is to delay the listener execution, and guarantee, that only one listener runs at a time, hence any code is free to change data, and it knows that no other code runs while it is doing so.

What about performance?

So it may seem that we are slow, since dirty-checking is inefficient. This is where we need to look at real numbers rather than just have theoretical arguments, but first let's define some constraints.

Humans are:

  • Slow — Anything faster than 50 ms is imperceptible to humans and thus can be considered as "instant".

  • Limited — You can't really show more than about 2000 pieces of information to a human on a single page. Anything more than that is really bad UI, and humans can't process this anyway.

So the real question is this: How many comparisons can you do on a browser in 50 ms? This is a hard question to answer as many factors come into play, but here is a test case: http://jsperf.com/angularjs-digest/6 which creates 10,000 watchers. On a modern browser this takes just under 6 ms. On Internet Explorer 8 it takes about 40 ms. As you can see, this is not an issue even on slow browsers these days. There is a caveat: The comparisons need to be simple to fit into the time limit... Unfortunately it is way too easy to add a slow comparison into AngularJS, so it is easy to build slow applications when you don't know what you are doing. But we hope to have an answer by providing an instrumentation module, which would show you which are the slow comparisons.

It turns out that video games and GPUs use the dirty-checking approach, specifically because it is consistent. As long as they get over the monitor refresh rate (typically 50-60 Hz, or every 16.6-20 ms), any performance over that is a waste, so you're better off drawing more stuff, than getting FPS higher.

Solution 2

Misko already gave an excellent description of how the data bindings work, but I would like to add my view on the performance issue with the data binding.

As Misko stated, around 2000 bindings are where you start to see problems, but you shouldn't have more than 2000 pieces of information on a page anyway. This may be true, but not every data-binding is visible to the user. Once you start building any sort of widget or data grid with two-way binding you can easily hit 2000 bindings, without having a bad UX.

Consider, for example, a combo box where you can type text to filter the available options. This sort of control could have ~150 items and still be highly usable. If it has some extra feature (for example a specific class on the currently selected option) you start to get 3-5 bindings per option. Put three of these widgets on a page (e.g. one to select a country, the other to select a city in the said country, and the third to select a hotel) and you are somewhere between 1000 and 2000 bindings already.

Or consider a data-grid in a corporate web application. 50 rows per page is not unreasonable, each of which could have 10-20 columns. If you build this with ng-repeats, and/or have information in some cells which uses some bindings, you could be approaching 2000 bindings with this grid alone.

I find this to be a huge problem when working with AngularJS, and the only solution I've been able to find so far is to construct widgets without using two-way binding, instead of using ngOnce, deregistering watchers and similar tricks, or construct directives which build the DOM with jQuery and DOM manipulation. I feel this defeats the purpose of using Angular in the first place.

I would love to hear suggestions on other ways to handle this, but then maybe I should write my own question. I wanted to put this in a comment, but it turned out to be way too long for that...

TL;DR
The data binding can cause performance issues on complex pages.

Solution 3

By dirty checking the $scope object

Angular maintains a simple array of watchers in the $scope objects. If you inspect any $scope you will find that it contains an array called $$watchers.

Each watcher is an object that contains among other things

  1. An expression which the watcher is monitoring. This might just be an attribute name, or something more complicated.
  2. A last known value of the expression. This can be checked against the current computed value of the expression. If the values differ the watcher will trigger the function and mark the $scope as dirty.
  3. A function which will be executed if the watcher is dirty.

How watchers are defined

There are many different ways of defining a watcher in AngularJS.

  • You can explicitly $watch an attribute on $scope.

      $scope.$watch('person.username', validateUnique);
    
  • You can place a {{}} interpolation in your template (a watcher will be created for you on the current $scope).

      <p>username: {{person.username}}</p>
    
  • You can ask a directive such as ng-model to define the watcher for you.

      <input ng-model="person.username" />
    

The $digest cycle checks all watchers against their last value

When we interact with AngularJS through the normal channels (ng-model, ng-repeat, etc) a digest cycle will be triggered by the directive.

A digest cycle is a depth-first traversal of $scope and all its children. For each $scope object, we iterate over its $$watchers array and evaluate all the expressions. If the new expression value is different from the last known value, the watcher's function is called. This function might recompile part of the DOM, recompute a value on $scope, trigger an AJAX request, anything you need it to do.

Every scope is traversed and every watch expression evaluated and checked against the last value.

If a watcher is triggered, the $scope is dirty

If a watcher is triggered, the app knows something has changed, and the $scope is marked as dirty.

Watcher functions can change other attributes on $scope or on a parent $scope. If one $watcher function has been triggered, we can't guarantee that our other $scopes are still clean, and so we execute the entire digest cycle again.

This is because AngularJS has two-way binding, so data can be passed back up the $scope tree. We may change a value on a higher $scope that has already been digested. Perhaps we change a value on the $rootScope.

If the $digest is dirty, we execute the entire $digest cycle again

We continually loop through the $digest cycle until either the digest cycle comes up clean (all $watch expressions have the same value as they had in the previous cycle), or we reach the digest limit. By default, this limit is set at 10.

If we reach the digest limit AngularJS will raise an error in the console:

10 $digest() iterations reached. Aborting!

The digest is hard on the machine but easy on the developer

As you can see, every time something changes in an AngularJS app, AngularJS will check every single watcher in the $scope hierarchy to see how to respond. For a developer this is a massive productivity boon, as you now need to write almost no wiring code, AngularJS will just notice if a value has changed, and make the rest of the app consistent with the change.

From the perspective of the machine though this is wildly inefficient and will slow our app down if we create too many watchers. Misko has quoted a figure of about 4000 watchers before your app will feel slow on older browsers.

This limit is easy to reach if you ng-repeat over a large JSON array for example. You can mitigate against this using features like one-time binding to compile a template without creating watchers.

How to avoid creating too many watchers

Each time your user interacts with your app, every single watcher in your app will be evaluated at least once. A big part of optimising an AngularJS app is reducing the number of watchers in your $scope tree. One easy way to do this is with one time binding.

If you have data which will rarely change, you can bind it only once using the :: syntax, like so:

<p>{{::person.username}}</p>

or

<p ng-bind="::person.username"></p>

The binding will only be triggered when the containing template is rendered and the data loaded into $scope.

This is especially important when you have an ng-repeat with many items.

<div ng-repeat="person in people track by username">
  {{::person.username}}
</div>

Solution 4

This is my basic understanding. It may well be wrong!

  1. Items are watched by passing a function (returning the thing to be watched) to the $watch method.
  2. Changes to watched items must be made within a block of code wrapped by the $apply method.
  3. At the end of the $apply the $digest method is invoked which goes through each of the watches and checks to see if they changed since last time the $digest ran.
  4. If any changes are found then the digest is invoked again until all changes stabilize.

In normal development, data-binding syntax in the HTML tells the AngularJS compiler to create the watches for you and controller methods are run inside $apply already. So to the application developer it is all transparent.

Solution 5

I wondered this myself for a while. Without setters how does AngularJS notice changes to the $scope object? Does it poll them?

What it actually does is this: Any "normal" place you modify the model was already called from the guts of AngularJS, so it automatically calls $apply for you after your code runs. Say your controller has a method that's hooked up to ng-click on some element. Because AngularJS wires the calling of that method together for you, it has a chance to do an $apply in the appropriate place. Likewise, for expressions that appear right in the views, those are executed by AngularJS so it does the $apply.

When the documentation talks about having to call $apply manually for code outside of AngularJS, it's talking about code which, when run, doesn't stem from AngularJS itself in the call stack.

Share:
355,093
Pashec
Author by

Pashec

Updated on August 17, 2022

Comments

  • Pashec
    Pashec almost 2 years

    How does data binding work in the AngularJS framework?

    I haven't found technical details on their site. It's more or less clear how it works when data is propagated from view to model. But how does AngularJS track changes of model properties without setters and getters?

    I found that there are JavaScript watchers that may do this work. But they are not supported in Internet Explorer 6 and Internet Explorer 7. So how does AngularJS know that I changed for example the following and reflected this change on a view?

    myobject.myproperty="new value";
    
    • Sotomajor
      Sotomajor about 12 years
      Be aware that since angular 1.0.0rc1 you need to specify ng-model-instant (docs-next.angularjs.org/api/…) to have your moder updated insantly. Otherwise it will be updated on blur event.
    • riffraff
      riffraff almost 12 years
      Marcello's link is apparently broken, so here it is again: github.com/mhevery/angular.js/blob/master/docs/content/guide‌​/…
    • Kevin Meredith
      Kevin Meredith over 10 years
      @orian, that link is bad. updated to (I assume) is the same - docs.angularjs.org/guide/databinding
    • aug
      aug about 9 years
      For those still reading this question, please note that Angular 2.0 has heavily changed how they go about databinding since Angular 1.x in order to work with web components and address a lot of the issues in the answers below.
  • AlexG
    AlexG about 12 years
    Wouldn't you get increased performances by using many smaller scopes ?
  • numan salati
    numan salati almost 12 years
    when is the apply method triggered?
  • user252690
    user252690 over 11 years
    The strategy is nice, but it's only working when the change is going through the AngularJS event loop. If the change is not performed as an AngularJS UI response event, e.g. a DOM event listener, it is not detected. But that's a fair compromise....
  • Jakob
    Jakob over 11 years
    @Misko Hevery: Could you give an example of when proxies are not semantically correct?
  • Supr
    Supr over 11 years
    @user252690 Normally DOM event listening should be done through Angular directives. DOM event listening, jQuery, and other DOM related code does not belong in "user code" in an Angular application. Instead, if Angular doesn't already support your needs, then you should implement a directive ("library code") to abstract it away from your "user code". In some cases you might feel that the cost of implementing a new directive out-weights the benefits (clean code, separation of concerns, reusability), in which case you can just wrap your event handling in $scope.$apply.
  • zcrar70
    zcrar70 over 11 years
    @Infeligo you should check out Rx.Js or Bacon.js - and Functional Reactive Programming (FRP) in general, as that aims to solve the exact problem you are describing.
  • mpen
    mpen over 11 years
    "Change listeners fire immediately on setter" -- They don't have to. Can't we just queue the listener to fire later, when the current function is done executing, the same as Angular, except without the need to poll hundreds or thousands of properties at the end?
  • Daniel Earwicker
    Daniel Earwicker about 11 years
    @Mark - yes, in KO you just add .extend({ throttle: 500 }) to wait 500 milliseconds after the last change event before acting on it.
  • Nate Bundy
    Nate Bundy about 11 years
    This entire answer is great other than "As long as they get 50 fps, any performance over that is a waste, since the human eye can not appreciate it, so you're better off drawing more stuff, than getting fps higher." That statement is completely incorrect depending on your application. The eye can definitely appreciate more than 50 fps, and as the various problems with VR show (read any of the latest from John Carmack or Michael Abrash, especially the latter's GDC 2013 VR talk), 50 fps is actually way too slow. Other than that, your answer is great. I just don't want misinformation spreading.
  • Lance
    Lance about 11 years
    Can you provide a link to info on video games using dirty checking for performance? I can't find anything about it.
  • David Rivers
    David Rivers about 11 years
    Please clarify what "us" means in "That means that you have 25us per comparison". User Sessions? Umbrella Stations? Uranus Scientists?
  • Thorgeir
    Thorgeir about 11 years
    @DavidRivers us is µs just like in utorrent 1µs = 0.000001s
  • Le Duc Duy
    Le Duc Duy about 11 years
    @JasonGoemaat Unless angular needs to get into its cycle (which will only happen when there is a angularized event in DOM or angluarized $http ajax call or $apply call) then it won't waste a tiny juice on background tabs to crunch for dirty checking
  • Jason Goemaat
    Jason Goemaat about 11 years
    Ah, I thought just changing a value in javascript would make the bindings change from seeing the chrome extension work, but it must be calling $apply()
  • Kamal Reddy
    Kamal Reddy almost 11 years
    This should be on angular docs. As of now Angular documentation is one of the most esoteric documentation I have ever seen.
  • Aditya M P
    Aditya M P almost 11 years
    No Misko... don't go there, that place where the argument of "anything above 50fps cannot be perceived" exists! But otherwise, thanks for the great answer :)
  • bguiz
    bguiz almost 11 years
    @ misko hevery : "Change listeners fire immediately on setter", this isn't true is all cases, you can have change listeneners which aggregate events, and trigger their listeners in the digest cycle.
  • Gregory Magarshak
    Gregory Magarshak almost 11 years
    On mobile phones, so much dirty-checking can pose a problem. Check out the articles that have come out about the speed of Javascript on mobile phones. The "update on every change" is not a necessary consequence of accessor methods, they can queue things just as well as the Angular digest cycle does. Except they will know exactly what changed, and not go through all the bindings. In fact, many mobile browsers support defineSetter and defineGetter which may help ease the syntax a great deal for marking things as potentially dirty.
  • Scott Silvi
    Scott Silvi over 10 years
    Yeah I second this. Our app's primary responsibility is to display connections between different entities. A given page might have 10 sections. Each section has a table. Each table has 2-5 typeahead filters. Each table has 2-5 columns, each with 10 rows. Very quickly we run into perf issues, and going with the "similar tricks" options.
  • DjebbZ
    DjebbZ over 10 years
    The articles @NateBundy is referring about VR and latency > 60fps : blogs.valvesoftware.com/abrash/…. You confirm NateBundy ?
  • Nate Bundy
    Nate Bundy over 10 years
    @DjebbZ All of blogs.valvesoftware.com/abrash (Michael Abrash's Valve blog) is indeed great reading if you're interested in VR. For some reason, your link doesn't work even though that article exists.
  • skusunam
    skusunam over 10 years
    We have similar issues with our data-grid with 50 as page size. I see that there are around 5-8k bindings registered on this page and as expected it performs poorly. @MW did you really try out the option you suggested in your post?
  • JD Smith
    JD Smith over 10 years
    @MW. - Sounds like you're saying that adding a "selected" class to a select option would add an additional data binding for every option. Wouldn't you just need one data binding on the select element itself to achieve this?
  • JD Smith
    JD Smith over 10 years
    OK, I actually ran three variants in JSPerf and I saw no appreciable difference between them. Test results are here, if you're interested: 1 2 and 3. Of course the functions are very basic; more logic moved to functions would change the equation. I'm also very interested to hear anyone else's take on this issue.
  • MW.
    MW. over 10 years
    @JDSmith: I agree with that approach. It's still a data binding per option, but of course it's better if this just has to check against a property on an object instead of running through a function.
  • Abadaba
    Abadaba over 10 years
    "Say you want to add items into an array, as you are looping to add, each time you add you are firing events on change, which is rendering the UI. This is very bad for performance" If you know what you are doing in Backbone or any other framework, you only react to the array set event (collection reset), not each individual item.
  • Joel
    Joel over 10 years
    As far as "alternatives", Facebook React simply does one-way binding as you are doing, but it does give adaptors to allow two-way binding but its obvious you are making it two-way. RactiveJS calculates the dependencies and supports two-way binding, which seems more ideal to me.
  • MW.
    MW. over 10 years
    @Joel: Thank you! I'm pretty solidly locked into AngularJS for this project, but I'll definately have a look at those before starting a new one.
  • MW.
    MW. over 10 years
    This talk at ng-conf actually adresses this issue, and gives some tips on how to handle it: youtube.com/watch?v=zyYpHIOrk_Y
  • joshkurz
    joshkurz over 10 years
    you could add all your items in a 0ms $timeout to fix that issue.
  • Jason Boyd
    Jason Boyd about 10 years
    Is it fair to say that Angular is not only about data binding and some apps may not want to use this feature for exactly the reasons others have cited? I think the approach of DI and modularity is itself worth a lot; having magic auto-binding is nice but in every existing implementation has trade-offs of performance. Angular's way is arguably superior for the majority of CRUD web apps, and people are just hitting a wall by trying to take it to extremes. It would be nice to have an alternate method of event listening supported, but maybe that's fundamentally too complex for a single framework?
  • Gaute Løken
    Gaute Løken about 10 years
    Angular now has one way and bind-once databinding to help with this problem. Furthermore it now has indexes for your repeater source, which lets you modify the list without rebuilding the dom for the entire content.
  • MW.
    MW. about 10 years
    @Mithon - The indexes are certainly an improvement. However, they only help when the DOM needs to be replaced, the amount of databindings is still the same. Bind-once support in the Angular core sounds great; do you have a link to share?
  • Gaute Løken
    Gaute Løken about 10 years
    @MW. Honestly I thought bind-once was in the core. But it appears it's not. It's just something you can do when writing your own directives, basically linking stuff without watching them. However there is a ux mod for it: github.com/pasvaz/bindonce
  • Saurab Parakh
    Saurab Parakh about 10 years
    This is best explanation I have read so far. I too wrote on this coolcoder.in/2014/03/how-databinding-works-in-angular-js.htm‌​l
  • adl
    adl about 10 years
    @EliseuMonar The digest loop runs as a result of some event or calling $apply(), it is not called periodically based on a timer. see How does AngularJS's $watch function work? and how does the binding and digesting work in AngularJS?
  • Eliseu Monar dos Santos
    Eliseu Monar dos Santos about 10 years
    @remi, I am not concerned about the last version of AngularJS. Are they already using proxies or Object.observe? If not, they are still in the dirty checking era, which builds a timed loop to see if model attributes have changed.
  • conical
    conical almost 10 years
    The statement could be easily said in reverse as "Dirty checking is a clever feature for a problem which knockout does not have". ES6 is using observables and angular is getting rid of dirty checking. The real world caught up to this answer and shown it to be false.
  • norbertas.gaulia
    norbertas.gaulia almost 10 years
    i had request to make app with 2000 +elements on page (big tables) and made it with angular, it turned out that binding and rendering is not 60ms its 30-40secs on latest machine, then switched to backbone and booom 30k+ elements, inline editing, bindings etc, ~5ms lag
  • R.Moeller
    R.Moeller almost 10 years
    In case of realtime updates using server push, even a digest lag of 50ms can be a problem as the user notices stuttering e.g. when scrolling whenever an event comes in
  • Tom Neyland
    Tom Neyland almost 10 years
    @norbertas.gaulia That would indicate a fault in your implementation, not with angular.
  • Seb
    Seb over 9 years
    50ms is not 50fps, it's 20fps and it's very slow. For 50fps the $digest should take max 20ms.
  • Crashworks
    Crashworks over 9 years
    "Anything faster than 50 ms is imperceptible to humans" is not true. In testing we have found our customers can easily distinguish between 50ms update latency (20fps) and 16.6ms update latency (60fps). Scenes running at the former speed consistently get poorer overall "how did it feel" ratings even when people did not consciously register the framerate.
  • Aurelio
    Aurelio over 9 years
    A shout from the future for anyone reading this: one time binding is now a core feature in Angular v1.3, read more here: docs.angularjs.org/guide/expression
  • Aurelio
    Aurelio over 9 years
    And as @Joel suggested above React can be a solution in such cases, especially because it's not an alternative to angular, but a tool you can integrate also in an existing Angular app. For anyone interested here's (reddit.com/r/javascript/comments/1oo1y8/… ) an interesting post on Reddit about React performance.
  • NiRUS
    NiRUS over 9 years
    this comment is perfect for Angular to defend against Ember.js or Sproutcore or anyother frameworks.. although latter frameworks are beautiful in its way and powerful if you leverage on their way of design.. Angular JS is a game changer in Webworld running in a sandboxed browsers..
  • Timo Türschmann
    Timo Türschmann over 9 years
    @Lucas you do one-time binding.
  • user137717
    user137717 about 9 years
    i've read that digest will run a max of ten times sitepoint.com/understanding-angulars-apply-digest
  • Jivan
    Jivan over 8 years
    Good answer overall but I couldn't disagree more on the "Anything more than that (2000 pieces of information) is really bad UI" part. As soon as you go into business software or social networking, 2000 pieces of information on the same page is just common. The typical Airbnb, Twitter, Facebook or SalesForce page potentially handles way more than 2000 pieces of information.
  • user2864740
    user2864740 over 8 years
    So I read this answer, and it doesn't explain how AngularJS really work other than a vague "remembers the value and compares it to previous value", but is digresses into a bash-fest on other implementations. Great .. as the question was not about such or even about performance.
  • user2864740
    user2864740 over 8 years
    Like "Misko already gave an excellent description of how the data bindings work" (actually, strike that; as his answer is mostly just a campaign against other approaches), this answer does not answer the question asked.
  • superluminary
    superluminary over 8 years
    Thanks @user2864740 - though it's right that Misko's answer should be top. He knows the framework better than anyone, and it's pretty cool that he engages with Stack Overflow..
  • user2864740
    user2864740 over 8 years
    I disagree that said answer should be at the top; there is a difference between knowing something and writing a relevant/detailed reply for a specific question. There are better ways to get accolades. Anyway ..
  • superluminary
    superluminary over 8 years
    @user2864740 - Misko is of course the creator of Angular, and a really nice guy as well. Without him we have no Angular. Thanks for the upvote :)
  • user2864740
    user2864740 over 8 years
    I don't doubt such is true, but questions questions and answers answers :)
  • sbedulin
    sbedulin over 8 years
    I read this answer 5 times and I still don't understand what's meant here.
  • strider
    strider over 8 years
    Nice answer covering how the dirty-check behaves and what its actually evaluating, one thing was not too clear in Misko's answer.
  • Sumesh Kuttan
    Sumesh Kuttan about 8 years
    very well explained. Also, would like to know 1) what are the events which triggers a digest cycle. 2) what does $apply actually does.
  • Mangu Singh Rajpurohit
    Mangu Singh Rajpurohit almost 8 years
    Superb and detailed answer. @superluminary, thanks for such answer. Moreover, after reading this answer, I come to the point that we must not add non-idempotent expression as an expression being watched.
  • kravemir
    kravemir over 7 years
    I have to downvote it, because It makes too naive and generalized assumptions. It should have been: for common end-users is 50 ms browser reaction time more then fast enough. However, there are uncommon end-users, which require faster reaction time, while working/manipulating with more data, on slower machines! You can't just generalize assumptions like that... (btw, I love and use AngularJS for small web-apps)
  • Faither
    Faither over 4 years
    @NateBundy 60 FPS differs from 50 for sure. frames-per-second.appspot.com set one for 60 and other for 48.