Question
How does data binding work in AngularJS?
It is reasonably clear how AngularJS propagates changes from a view to a model. However, how does AngularJS detect changes to model properties without JavaScript property getters and setters?
For example, how does AngularJS know that this assignment occurred and update a bound view?
myobject.myproperty = "new value";
I have read about JavaScript watchers, but some older browser features are not supported in Internet Explorer 6 and Internet Explorer 7. How does AngularJS detect model changes in those environments?
Short Answer
AngularJS 1.x uses dirty checking, not property setters, getters, or browser-level property-change events. It repeatedly evaluates registered watch expressions during a digest cycle and compares each current value with its previously recorded value. When a value differs, AngularJS updates bindings and runs relevant listeners. Changes made through AngularJS-managed events automatically start a digest; changes from external code usually require $scope.$apply() or an AngularJS wrapper such as $timeout.
Concept
AngularJS data binding is built around three closely related ideas:
- A model: values stored on a scope, such as
user.name. - A watcher: a function that reads a value and remembers its previous result.
- A digest cycle: the process in which AngularJS runs watchers and looks for changed values.
For a template binding such as:
<p>{{ user.name }}</p>
AngularJS creates a watcher that can evaluate user.name. During a digest, it evaluates that expression, compares the result with the previously stored result, and updates the DOM if the value changed.
AngularJS 1.x does not need setters or getters for ordinary data binding. It does not intercept this assignment:
$scope.user.name = "Ada";
Instead, at the next digest, AngularJS evaluates $scope.user.name again and sees that it no longer matches its old value. This comparison-based approach is called dirty checking.
A digest normally begins after work AngularJS knows about, including:
- AngularJS directives such as
ng-clickandng-model - AngularJS services such as
$http,$timeout, and$interval - Calls made through
$scope.$apply()
This design worked in older browsers, including IE 6 and IE 7, because it relies on normal JavaScript expression evaluation rather than unsupported property observation APIs.
AngularJS is an end-of-life framework, but understanding its digest cycle is important when maintaining existing AngularJS applications.
Mental Model
Imagine AngularJS as an inspector making regular rounds through a checklist.
Each watcher is one item on the checklist:
“What is the current value of
user.name?”
On the previous round, the inspector recorded "Grace". On the next round, it sees "Ada". Because the value is different, AngularJS knows something changed and refreshes the parts of the page that display it.
The assignment itself does not notify AngularJS:
$scope.user.name = "Ada";
AngularJS notices only when it performs its next inspection round: the digest cycle.
$apply() is like telling the inspector, “External code has finished changing things; please make a round now.”
Syntax and Examples
Use scope properties in templates, and let AngularJS start digests from its directives and services.
<div ng-app="app" ng-controller="ProfileController">
<p>Hello, {{ user.name }}!</p>
<button ng-click="rename()">Rename</button>
</div>
angular.module("app", [])
.controller("ProfileController", function ($scope) {
$scope.user = {
name: "Grace"
};
$scope.rename = function () {
$scope.user.name = "Ada";
};
});
When the button is clicked:
- AngularJS runs
rename()becauseng-clickbelongs to AngularJS. - The function changes
$scope.user.name. - AngularJS starts a digest after the click handler finishes.
- The watcher for
{{ user.name }}sees the new value. - The paragraph changes to
Hello, Ada!.
To react to a value in JavaScript, register a watcher explicitly:
$scope.$watch("user.name", function (newValue, oldValue) {
if (newValue !== oldValue) {
console.log("Name changed from", oldValue, "to", newValue);
}
});
A watch expression may be a string expression or a function:
$scope.$watch(
function () {
return $scope.user.name;
},
function (newValue, oldValue) {
console.log(newValue, oldValue);
}
);
Use $apply() when an external library or raw browser callback modifies scope data:
externalLibrary.on("profileUpdated", function (name) {
$scope.$apply(function () {
$scope.user.name = name;
});
});
Do not call $apply() from code already invoked by AngularJS, such as an ng-click handler; AngularJS already schedules the digest there.
Step by Step Execution
Consider this controller and template:
<p>{{ account.status }}</p>
<button ng-click="activate()">Activate</button>
$scope.account = { status: "Pending" };
$scope.activate = function () {
$scope.account.status = "Active";
};
Here is what happens step by step:
- AngularJS links the template to the scope.
- It registers a watcher for the expression
account.status. - During the first digest, the watcher evaluates to
"Pending". - AngularJS stores
"Pending"as the last known value and renders it in the paragraph. - The user clicks the button.
ng-clickcallsactivate().activate()assigns"Active"to$scope.account.status.- Because
ng-clickis AngularJS-managed, AngularJS starts a digest. - The watcher evaluates
account.statusagain and now receives"Active". - The previous value and current value differ, so AngularJS updates the paragraph.
- AngularJS repeats watcher checks until no watched value changes during a pass. This stabilization prevents dependent bindings from being left out of date.
If a watcher keeps changing a watched value every time it runs, AngularJS cannot stabilize and eventually throws a digest iteration error. Watch listeners should not continually mutate the values they watch.
Real World Use Cases
AngularJS dirty checking and digest scheduling appear in common application tasks:
- Forms:
ng-modelcopies user input into scope data, then bound validation messages and previews refresh. - HTTP requests:
$http.get()receives server data, updates a controller model, and AngularJS refreshes the view. - Timers:
$timeout()changes a countdown or notification state and automatically triggers a digest. - Third-party widgets: A map, payment widget, WebSocket client, or legacy jQuery plugin may invoke callbacks outside AngularJS. Wrap model updates in
$apply()or use$timeout(). - Live dashboards: New metrics update scope values; bindings redraw text, classes, and repeated rows.
- Validation rules: A watcher can derive one field from another, although computed values are often clearer when calculated directly in controller methods or services.
Example with a WebSocket-style external callback:
socket.onmessage = function (event) {
var message = JSON.parse(event.data);
$scope.$apply(function () {
$scope.latestMessage = message.text;
});
};
Without $apply(), the value may change in memory but the AngularJS view may not refresh until some later AngularJS event happens.
Real Codebase Usage
In production AngularJS code, developers generally avoid manually triggering digests unless integrating external code.
Prefer AngularJS-aware services
Use $timeout rather than the browser's setTimeout when the callback changes scope data:
$timeout(function () {
$scope.message = "Saved";
}, 500);
$timeout schedules AngularJS-aware work, so the view updates automatically.
Integrate external callbacks carefully
For callbacks from non-AngularJS libraries, call $apply() around the model update:
mapWidget.onMarkerSelected(function (marker) {
$scope.$apply(function () {
$scope.selectedMarker = marker;
});
});
If a digest may already be running, $evalAsync() is often safer for scheduling scope work:
mapWidget.onMarkerSelected(function (marker) {
$scope.$evalAsync(function () {
$scope.selectedMarker = marker;
});
});
Use guard clauses in watchers
A watch listener may run during initialization, so avoid unnecessary work:
$scope.$watch("searchTerm", function (newValue, oldValue) {
if (newValue === oldValue) {
return;
}
searchService.recordSearch(newValue);
});
Keep watches inexpensive
Every watcher is evaluated during digest cycles. Real applications should avoid expensive computations directly in templates or watch functions. Compute reusable values once when appropriate, use one-time bindings for static values, and remove watchers that are no longer needed.
var stopWatching = $scope.$watch("session.user", function (user) {
if (user) {
initializeUser(user);
stopWatching();
}
});
Common Mistakes
Expecting a plain assignment to update the view immediately
This external callback changes the model, but AngularJS may not know to run a digest:
setTimeout(function () {
$scope.status = "Complete";
}, 1000);
Use $timeout instead:
$timeout(function () {
$scope.status = "Complete";
}, 1000);
Or use $apply() when you must use an external callback:
setTimeout(function () {
$scope.$apply(function () {
$scope.status = "Complete";
});
}, 1000);
Calling $apply() inside ng-click
This is incorrect because AngularJS is already processing an AngularJS event:
$scope.save = function () {
$scope.$apply(function () {
$scope.saved = true;
});
};
It can cause an [$rootScope:inprog] error. Write the assignment directly:
$scope.save = function () {
$scope.saved = true;
};
Creating watchers that modify their own watched value
This can prevent the digest from stabilizing:
$scope.$watch("count", function () {
$scope.count++;
});
Watch listeners should not repeatedly change the same watched data. Make updates in response to user actions or ensure the listener has a terminating condition.
Assuming $watch deeply tracks all object changes by default
A normal watch compares object references. Mutating a nested property may not be detected by a reference watch:
$scope.$watch("settings", function () {
console.log("Settings reference changed");
});
$scope.settings.theme = "dark";
For a small object where deep watching is truly needed:
$scope.$watch("settings", function () {
console.log("A nested setting changed");
}, true);
Deep watches can be expensive. Prefer watching a specific property such as settings.theme when possible.
Doing expensive work in templates
Avoid calling costly functions in bindings because AngularJS may evaluate them many times:
<!-- Avoid if calculateTotal is expensive -->
<p>{{ calculateTotal() }}</p>
Calculate the value at an appropriate point and bind the resulting property instead.
Comparisons
| Approach | How changes are detected | Typical AngularJS use |
|---|---|---|
| Dirty checking | AngularJS reevaluates watched expressions and compares old and new values | Standard AngularJS 1.x binding |
| Getters and setters | A property assignment runs custom code immediately | Not required for AngularJS scopes |
| Browser property observation | The browser notifies code when a property changes | Not the basis of AngularJS 1.x binding; older browser support was limited |
$watch | Registers a scope expression to compare during digests | Reacting to a particular model value in JavaScript |
$watchCollection | Shallowly observes array items or object properties | Lists and small collections where items may be added or removed |
Deep $watch(..., true) | Compares nested object data | Small objects only; use cautiously because it costs more |
$apply() | Runs external work, then starts a digest | Integrating non-AngularJS callbacks |
$digest() | Checks the current scope and its descendants | Lower-level API; usually avoid in application code |
$apply() is usually preferred over directly calling $digest() because $apply() begins from the root scope and includes AngularJS error handling. In everyday application code, AngularJS services and directives should start most digests for you.
Cheat Sheet
- AngularJS 1.x binding uses dirty checking.
- A watcher evaluates an expression and compares its current value with its previous value.
- A digest cycle runs watchers until values stabilize.
- AngularJS does not need property setters/getters to notice normal model changes.
ng-click,ng-model,$http,$timeout, and$intervaltrigger AngularJS-aware updates.- External callbacks do not necessarily trigger a digest; use
$apply()or$evalAsync().
// Register a watcher
$scope.$watch("user.name", function (newValue, oldValue) {
// Respond to a changed value
});
// External callback integration
externalApi.onChange(function (value) {
$scope.$apply(function () {
$scope.value = value;
});
});
// AngularJS-aware delay
$timeout(function () {
$scope.value = "updated";
}, 1000);
Rules:
- Do not call
$apply()from an AngularJS event handler. - Avoid expensive watch expressions and deep watches on large data.
- Watch specific properties instead of entire objects when possible.
- Do not mutate a watched value indefinitely from its own listener.
FAQ
Does AngularJS use getters and setters for data binding?
No. AngularJS 1.x primarily uses dirty checking: it reevaluates watch expressions during digest cycles and compares their values.
What triggers an AngularJS digest cycle?
AngularJS directives and services commonly trigger it, including ng-click, ng-model, $http, $timeout, and $interval. External library callbacks may require $apply().
Why does my AngularJS view not update after setTimeout?
The browser's setTimeout is outside AngularJS. Use $timeout, or wrap the scope update in $scope.$apply().
What is the difference between $apply() and $digest()?
$apply() evaluates code and starts a root-level digest. $digest() checks only the current scope and its descendants. Application code generally uses $apply() only for external integration.
Does $watch detect nested object changes automatically?
Not by default. A standard watch normally compares object references. Watch a specific nested property, use $watchCollection for shallow collection changes, or use deep watching carefully.
Why can too many watchers hurt AngularJS performance?
AngularJS evaluates watchers during digest cycles. More watchers, deep comparisons, and expensive expressions increase the work required for each update.
Does AngularJS support Internet Explorer 6 and 7 because of watchers?
Its dirty-checking approach does not depend on modern property-observation features. Historical AngularJS browser support still depended on the specific AngularJS version and application requirements.
Mini Project
Description
Build a small AngularJS status panel that updates a displayed connection state. It demonstrates the difference between an AngularJS-managed update and an update received from an external callback.
Goal
Display a connection status and safely refresh it when an external status event arrives.
Requirements
- Create a controller with an initial connection status of
Disconnected. - Display the status using an AngularJS template binding.
- Add a button that changes the status through
ng-click. - Simulate an external status callback that changes the status after a delay.
- Ensure the external update is visible in the view.
Keep learning
Related questions
Abort Ajax Requests with jQuery jqXHR.abort()
Learn how to cancel an in-progress jQuery Ajax request with jqXHR.abort(), handle abort status safely, and avoid stale UI updates.
Access the Correct this Inside a JavaScript Callback
Learn why JavaScript this changes in callbacks and how to preserve an object context using bind, arrow functions, and event handler patterns.
Add Key-Value Pairs to JavaScript Objects
Learn how to add key-value pairs to JavaScript objects with dot and bracket notation, dynamic keys, examples, and common mistakes.