We hear so much about Angular’s dirty checking mechanism and two-way data binding that they seem to be synonymous with it. So what the hell is this on a programming level?
The interface may be updated when a property of $scope is changed. Why does Angular change the interface by modifying a property on $Scope? This is determined by Angular’s data response mechanism. In Angular, this is dirty checking. And dirty check, and bidirectional binding is inseparable.
As an aside, there’s an interesting interface in JavaScript that when you modify (or add) a property of an object, it fires a setter in that object. If you’re not familiar with this, take a look at Object.defineProperty, including the very popular vuejs implemented through this interface in the last two years. It is an ES5 standard interface.
We can design an implementation that, when you modify or assign a property of $scope, triggers the setter for the JS object $Scope. We can customize this setter and call some logic inside the setter function to update the interface. Also, to make sure that the new object can be listened to, when you assign, you also need to modify the assigned object to be listened to.
Two-way binding as the name implies is two processes, one is to bind the $scope attribute value to the HTML structure, when the $scope attribute value changes when the interface changes; The other is that the $scope property automatically changes when the user performs an action on the interface, such as clicking, typing, or selecting (the interface may also change). The function of the dirty check is to “cause the interface to change when the value of the $scope property changes”.
Angular’s data response mechanism
So, at the code level, how does Angular listen for data changes and then update the interface? The answer is that Angular does not listen for data changes at all. Instead, it iterates over all $scopes at the appropriate time, checking for changes in their property values, and if so, iterating again with a variable, dirty, true, until one of the traversals is complete. When none of these $scope property values has changed, the traversal ends. Because a dirty variable is used as a record, it is called the dirty check mechanism.
There are three problems:
- When is the “right time”?
- How do I know if the value of an attribute has changed?
- How is this traversal loop implemented?
To solve these three problems, we need to dig into Angular’s $watch, $apply, and $digest.
$watch binds the value to be checked
To put it simply, when a scope is created, Angular parses the template structure of the current scope in the template, automatically finds interpolations (such as {{text}}) or calls (such as ng-click=”update”), and creates bindings with $watch. Its callback function is used to determine what to do if the new value is different (or the same) from the old value. Of course, you can manually bind a property in a script using $scope.$watch. It can be used as follows:
$scope.$watch(string|function, listener, objectEquality, prettyPrintExpression)Copy the code
The first argument is a string or function, and if it is a function, you need to run it to get a string that determines which property on $scope will be bound. The listener is a callback function that is executed when the value of this property changes. ObjectEquality is a Boolean that, when true, will perform a deep check on an object. The fourth parameter is how to parse the expression of the first parameter, the use of more complex, generally not passed.
$digest Traverses recursion
When the property to be checked is bound with $watch, the callback function is executed when the property changes. But as mentioned, Angular doesn’t have a listener, so how can it be called back? Instead of using the setter mechanism for object, it uses the dirty check mechanism. At the heart of dirty checking is the $digest loop. When the user does something, Angular calls $digest() internally, causing the interface to be rerendered. So what’s it all about?
After calling $watch, the corresponding information is bound to an angular internal? Watchers, it is a queue (array), and when $digest is triggered, Angular iterates through the array with a dirty variable. When it changed, dirty was set to true. At the end of $digest, it checks dirty again. If dirty is true, it calls itself again until dirty is true. But to prevent endless loops, Angular says that when a recursion occurs 10 or more times, it throws an error and breaks out of the loop.
The recursive process is as follows:
- Determines whether dirty is true, and if false, no $digest recursion is performed. (Dirty defaults to true)
- Traverse? Watchers, retrieve the old and new values of the corresponding attribute values
- Compare old and new values according to objectEquality.
- If the two values are different, proceed. If the two values are the same, set dirty to false.
- After checking all the Watcher, if dirty is still true (read my pseudocode below for this)
- Set dirty to true
- Replace the old value with the new value, so that in the next round of recursion, the old value is the new value for this round
- Call $digest again
When the recursive process is complete, $Digest also executes:
- Rerender the changed $scope into the interface
When a scope is created, $scope.$digest is run once. The default value for dirty is set to true, so if you use $watch in a Controller and assign it to a property, you will often refresh the page to see the $watch callback executed. But now, how does angular internally call $digest()?
$apply trigger $digest
Instead of using $digest directly when we program ourselves, we call $scope.$apply(), which triggers a $Digest recursive traversal inside $apply. In the meantime, you can pass $apply an argument, a function, that will be executed before $digest starts. Now back to the above question, how does Angular trigger $digest internally? In fact, Angular requires you to use ng-click, ng-modal, ng-keyup, etc., to bind data in both directions. Why? Because these Angular directives encapsulate $apply, such as ng-click, It contains the document. The addEventListener (‘ click ‘) and $scope. $the apply ().
When a user uses ng-click in a template, it looks like this:
<div ng-click="update()">change</div>Copy the code
$scope.update = function() {
$scope.name = 'tom'
}Copy the code
In fact, when the user clicks, Angular also executes $scope.$apply() internally, triggering the $Digest traversal recursion that eventually triggers the interface redraw.
Call $apply manually
There are two cases in which we need to call $apply manually. One is when we call angular syntaxes such as $HTTP and $timeout, and the other is when we update $scope without using angular internals. For example we use $element.on(‘click’, () => $scope.name = ‘Lucy ‘). $apply = $scope = $scope = $scope = $scope = $scope = $scope = $scope = $scope = $scope
function($timeout) {// When we pass on('click') can be done when certain updates are triggered$timeout(() = > {$scope.name = 'lily'}) // You can also do this$element.on('click', () = > {$scope.name = 'david'
$scope.$apply()})}Copy the code
However, it is important to note that you should never call $apply manually during recursion, such as in ng-click functions, or in the $watch callback function.
Pseudocode implementation
You probably already know about angular dirty checking, but we want to go a little deeper and clarify things in code. Instead of copying the Angular source code, I’ll write my own pseudocode to help understand the mechanism.
import { isEqual } from 'lodash'
class Scope {
constructor() {
this.$$dirty = true
this.$$count = 0
this.$$watchers= []}$watch(property, listener, deepEqual) {
let watcher = {
property,
listener,
deepEqual,
}
this.$$watchers.push(watcher)
}
$digest() {
if (this.$$count >= 10) {
throw new Error('$Digest more than 10 times')
}
this.$$watchers.forEach(watcher => {
let newValue = eval('return this.' + watcher.property)
let oldValue = watcher.oldValue
if (watcher.deepEqual && isEqual(newValue, oldValue)) {
watcher.dirty = false
}
else if (newValue === oldValue) {
watcher.dirty = false
}
else {
watcher.dirty = true
eval('this.' + watcher.property + '='NewValue) watcher.listener(newValue, oldValue) // Note that the listener is assigned to the newValue$scopeOldValue = newValue} // This implementation is a little different from Angular logic. Angular listener is called when both newValue and oldValue are undefined. Maybe it's in Angular$watch"Will be automatically given$scope}) this.$$count ++
this.$$dirty = false
for (let watcher of this.$$watchers) {
if (watcher.dirty) {
this.$$dirty = true
break}}if (this.$$dirty) {
this.$digest()}else {
this.$patch()
this.$$dirty = true
this.$$count= 0}}$apply() {
if (this.$$count) {
return/ / when$digestCannot be triggered during execution$apply
}
this.$$dirty = true
this.$$count = 0
this.$digest()}$patch() {// redraw interface}}Copy the code
function ControllerRegister(controllerTemplate, controllerFunction) {
let $scope = new Scope()
$paser(controllerTemplate, $scope// Parse the controller's template, parse all the attributes in the template, and assign these attributes to$scope
controllerFunction($scope) // Inside controllerFunction may give another call$scopeSome properties have been added. Note that they cannot be called while controllerFunction is running$scope.$apply(a)let properties = Object.keys($scope) / / find out$scope// All attributes on the$scopeProperties = properties.filter(item => item.indexof ('$')! == 0) // Of course, this exclusion method only ensures that properties. ForEach (property => {$scope.$watch(property, () => {}, true)})$scope.$digest()}Copy the code
This is a pseudo-code implementation of angular’s internal mechanism. It can’t be used as a real engine, but it shows the whole idea of dirty checking.
The article from my blog: https://www.tangshuang.net/5435.html