We have seen in the previous post how the parser constructs an expression tree and we quickly mentioned that the tree is executed from the bottom up. In this post we will examine further how the expression tree is evaluated and then look at how the tree is re-evaluated when an observable object notifies that a change has occurred.
Expressions are used in two ways. When a new binding is first created the expression must be evaluated once to determine how to initially display the UI. The binding is then monitored for changes and partially re-evaluated as necessary to update the UI as the program runs. To perform the initial evaluation the expression is parsed into the expression tree and the evaluate method on the root of the tree is invoked. This method calls invoke on its child expressions and then performs whatever action is needed on the values the child expressions return before then returning its own evaluated value. So although the sequence of execution starts at the top of the tree the path of the execution immediately recursively descends to the leaf of the tree and then evaluates upwards from there. Each expression object keeps a copy of the resulting value for later.
The diagram below shows the example expression from the previous article annotated to show how the expression is evaluated and the cached values in each expression:
As this initial evaluation occurs the expressions may encounter a value that is an observable object. When this happens the expression adds a notification handler to the observable object. This allows the expression to be notified when a model value changes.
In the example above we will say that the data context ($d) is an observable object so the VariableReferenceExpression will return the data context when it is initially evaluated and the DereferenceExpression will see that this value is observable and will add a notification handler to the this object. When the model notifies that the value has changed the notification handler will be called and the DereferenceExpression will re-evaluate its value by calling the val member of the data context again. If the new value is the same as the previous value the execution will stop, if it isn't the DereferenceExpression will call its parent expression with the new value.
This will cause the BinaryOperatorExpression to subtract the new value of the right hand DereferenceExpression from the value that was previously returned from the left hand BinaryOperatorExpression and return the new value to its parent. This will then cause the UI to be updated.
Note how at every step the expressions hold copies of the previous values and they will only re-evaluate when something changes and will use the cached copies as much as possible to re-evaluate changes. This ensures that the minimum amount of work has to be done when a change occurs and also ensures that the model is not hit with a cascade of calls when a single value changes.
That was quite complicated - How about another diagram. This shows the execution path when the model notifies that the value has changed.
As you can see when the model notifies that the val property has changed all that happens is the DereferenceExpression reads in the new value and then passes it up to the BinaryOperatorExpression that then re-adds the new value to the cached value from the left hand side of the expression and then passes the new value up to the UI. This avoids the need to re-calculate all of the rest of the expression.