Why I hate(d) data binding
Over the years, I have helped developed many front-end applications, with the largest and most complicated ones mostly in .NET (Windows Forms, WPF, and Silverlight). Like many .NET developers, WPF (back in 2007) was my first experience with data binding.
Data binding was a powerful concept, and it made the tortuously mundane incredibly easy. For the simple, highly error-prone boilerplate code that often comprised a significant part of your average Windows Forms application, WPF was a boon to many because it allowed you to write less code for these cases. However, as we built larger and larger applications with WPF and Silverlight, flaws started to appear.
Taking advantage of the productivity gains WPF promised typically required massive adaptations (or more frequently, rewrites) of existing systems. WPF/Silverlight (and today’s Windows Store API apps, are generally all-or-nothing propositions. Working with UI code that assumed Windows Forms APIs (for example, Control.BeginInvoke instead of Dispatcher.BeginInvoke) was a nightmare. The two technologies, invented by the same company for the same operating system, did not play along very well.
Performance concerns also plagued WPF from its inception. While it has gotten better over the years, there are longstanding problems with its approach to some problems. One of my all-time favorites were innocuous blocks of code like this:
foreach (var item in someMsg.items) { this.myObservableCollection.Add(item); } |
As it turns out, this is one of the easiest and most common ways to grind your WPF application to a halt. What if you have thousands of items in that message (like, say orders in a blotter) and that collection is data-bound? You’ve raised thousands of events, and your app will hang for several seconds. Similar problems appear when trying to update random properties of objects in those collections; do it in a tight loop, and your application hangs.
And then, because the data binding framework was all-encompassing, it was exceedingly difficult to actually interact with views when you needed to. After working so hard to keep the viewmodel/controller/whatever so agnostic from the view, sometimes you needed to write awful code like IView.close(), where IView was a wrapper around a control injected into the viewmodel, so that you could actually get what you needed to get done, done. Simple things like trying to fade out items in a view that were disappearing because they were removed from the underlying collection became next to impossible, because without a view model providing state, views were powerless.
After a few years of writing increasingly obtuse attached behaviors (just flip back in this blog for some real horror shows) and being limited by a data binding framework with fundamental design flaws that forced me to work around it in order to get acceptable performance, it became clear to me that data binding could never be the answer; it was an abstraction too far, and would always lead you down painful performance problems. I was done with data binding.
Enter Backbone
Around the same time, I started doing more JavaScript, and mostly using Backbone. After years of being suffocated by WPF’s confining patterns and practices, Backbone was a bit of fresh air. Here, again, I had the power to freely manipulate views, and I wasn’t terribly far from the DOM. If I wanted my web app to be blindingly fast, I could roll as much custom code as I needed to, and if I wanted to write any kind of saucy visual effects, I could do that. Backbone didn’t promise much, so it never failed me.
It wasn’t a perfect scenario. JavaScript doesn’t have a native way of loading modules, and none of the solutions out there made mocking particularly easy. Dependency injection was something widely sneered at by many in the JavaScript space (and Python and Ruby as well, for that matter); dependency injection was something that was only ever supposed to matter for static languages because having a type system forced you to do that. After all, in JavaScript, if you want an object’s methods to behave a certain way, you just reassign them, no biggie.
Backbone’s complete lack of a data binding solution is also something that most developers coming from .NET (or any other environment, really) just can’t abide by. I’ve helped a number of JavaScript projects already in-progress that either bring in Backbone Stickit or tried mixing it with Knockout. The same kinds of performance and design problems that we used to have with WPF would come creeping back (oh no, I have a thousand rows and my website is now wasting all of its cycles endlessly updating the DOM). The non-library alternative, writing a ton of manual code from Backbone.Model.on(‘change’) handlers to views, inevitably led to poor-performing spaghetti because you ended up with the performance characteristics of a data binding framework coupled with the excessive code that data binding is supposed to free you from.
The “correct” answer with Backbone is to be far more disciplined about when, exactly, the DOM is updated. Don’t blindly tie view updates to model change events; think very hard and very carefully about where you’re updating the DOM, and if you’re really concerned about performance, try to ensure that you’re only updating the DOM as few times as you can. But this is incredibly difficult to test properly because your code references the DOM all over the place. There are solutions, but they are awkward.
AngularJS—we’re talking data binding again?
AngularJS has been around since Backbone has, but it historically hasn’t been as good as Backbone about delivering what it promised, mostly because AngularJS is far more ambitious. Where Backbone steps back and lets you make all the decisions you want, AngularJS has very strong suggestions about how things should be done, and it seems to punish you when you make a mistake in how you use it. For me, this sounded awfully familiar and not at all fun. Older versions were also feature incomplete, somewhat slow, and buggy. Why bother?
As it turns out, the AngularJS team made some fundamental design decisions that differ substantially from other data binding frameworks. The more that I’ve seen others use it (and then later, myself), the more I’m convinced that Angular’s unique approach to data binding is the correct one, particularly for JavaScript, and that the rest of the utilities that it provides (dependency injection in particular) are essential to proper building of large-scale JavaScript applications.
But to get there, I had to clear up a few misconceptions that I had.
Angular takes the DOM away from you
This is only as true as you want it to be. When you want it:
<div ng-class="{positive: quantity > 0, zero: quantity == 0, negative: quantity < 0 }">{{quantity}}</div> |
…does exactly what you want it to.
And, if for whatever reason, you wanted to do it mostly in code:
<div class="quantity" quantity-colorizer></div> |
function QuantityColorizerDirective () { return { link: function (scope, element, attrs) { scope.$watch('quantity', function (quantity) { applyQuantityClass(element, quantity); }); } }; } function applyQuantityClass (element, quantity) { if (quantity > 0) { element.addClass('positive'); element.removeClass('zero negative'); } else if (quantity < 0) { element.addClass('negative'); element.removeClass('zero positive'); } else { element.addClass('zero'); element.removeClass('positive negative'); } } |
Note that the meat of the code, applyQuantityClass, is completely Angular-agnostic. If you don’t want to (or can’t) take advantage of Angular data-binding, then write your own fully Angular agnostic code and do that instead.
Angular wraps everything for no reason
As it turns out, there is a very specific reason for wrapping document, window, setTimeout, and others—dependency injection and testing. Code that references globals like these directly can never truly be mocked.
And $window == $(window), $document == $(document), and $setTimeout(fn, ms, false) == setTimeout(fn, ms); if using the DI container to provide these variables is truly off-putting, introduce globals into your code instead that you wrap with jQuery.
Wrapping everything in Angular’s $apply is onerous
Yes, it is, but you might be overdoing it. scope.$apply is used to start a digest cycle; this is when Angular re-evaluates all of the binding expressions on the page, and if they’ve changed, updates them. If it sounds expensive, it’s because it is. But as it turns out, there are several advantages to this choice:
- It breaks the one-to-one mapping of model changes to DOM updates. Updating the DOM is expensive, and batching up those updates is essential to a well-performing web app. Update as much of the model as you can, and then call $apply. Ideally, your controllers are doing most of your model manipulation, and your controllers are always called from $apply, rather than hopping back and forth across Angular’s digest cycle by littering your controller code with $apply. As much as ng-repeat is widely panned for being slow, collection binding in other frameworks is much more expensive. (Try adding items in a loop in Angular and in any other data binding framework where the collection is bound to a UI and you’ll see what I mean.)
- Not having an Angular base-class model means you can use anything for your models. No Backbone.Model.extend or Object.createProperty required; just update your models, and don’t worry about raising change events all over the place.
(This is a more detailed answer for the advantages of Angular’s method of updating the DOM over what most other data binding frameworks do: http://stackoverflow.com/questions/9682092/databinding-in-angularjs/9693933#9693933)
Angular doesn’t wrap all of the DOM events
See “Angular takes the DOM away from you” above; if you want to do something that Angular hasn’t anticipated, you write a directive and you write exactly the same DOM code you would have had to write anyway. It saved you time where it could, and you spend exactly the same amount of time where it couldn’t.
There’s also always Angular UI.
Angular documentation is terrible
This is the only point on the list that I still have trouble with. The amount of available documentation isn’t proportional to the amount of functionality that Angular provides. That’s a problem, but hopefully as more people start using it and writing novella blog posts like this one, this will change.
Angular directives are hard to write
The first ones are disastrously difficult, no question about it—especially given how terrible the documentation can be at times. Fortunately, this got easier with time. Just knowing what the plain, give-it-to-me-straight-just-like-Backbone-does directive looks like and starting from there helps you down the route of creating reusable components that are ultimately more powerful and straightforward to consume than the equivalent Backbone view(s) would be.
Angular is slow
If there is a particular hotspot in your application that is unacceptably slow with Angular (and you’re not excessively calling $apply or overusing $watch), then you can always rewrite that hotspot as a directive this brings you exactly where you were if you didn’t have Angular at all. But again, Angular has saved you time where it could, and gets out of your way when it can’t.
Angular has dependency injection and I don’t like it
Dependency injection may be somewhat unusual in dynamic languages, but it doesn’t have to be. Being able to mock the document is incredibly useful for testing. But, again, if you don’t want to mock your globals, then you’re not going to be incredibly moved by this point.
Angular takes over my app
It doesn’t have to; through judicious calls to angular.bootstrap, you can use it as little as you want or as much as you want. But being intimately involved with the markup of your application is the price you pay for being able to write custom components in markup.
I need server-side rendering because I need my page to be accessible by search engines
Actually, that’s probably not true (via @domenic).
AngularJS made me okay with data binding again
I’m glad that JavaScript frameworks continue to push the boundary on trying to offer simple, fast, and flexible, and powerful all in the same framework. AngularJS might just be the first that I’ve seen that actually allows you to succeed at all four without violating its coding guidelines. It’s nice to see that data binding, as a strategy, isn’t as fundamentally flawed as I had feared for a while, and as many implementations would have led me to believe. But the JavaScript space changes quickly, especially these days (Ember is about to turn 1.0 any day now); it’ll be exciting to see how this space continues to evolve, and to see fresh approaches to the problems that we have always had.