Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Multiple statements in angular expression

Tags:

angularjs

I'm playing around with Angularjs, and possibly I am abusing it. I am using semicolon to have several statements in an angular expression like this (jsFiddle):

<tr ng-repeat="i in [1, 2, 3, 4]">
  <td>i = {{ m = k; k = j; j = i + 1; i}}</td>
  <td>j = {{j}}</td>
  <td>k = {{k}}</td>
  <td>m = {{m}}</td>
</tr>

At first, I thought that k would have the value of j before the computation i+1, but apparently it does not work like this. The result is:

i = 1   j = 2   k = 2   m = 2
i = 2   j = 3   k = 3   m = 3
i = 3   j = 4   k = 4   m = 4
i = 4   j = 5   k = 5   m = 5

So apparently assigning jto k and k to m, doesn't mean values are copied but that these names are bound together. I can understand that. But something strange happens if I remove the line that display the value of k (jsFiddle):

<tr ng-repeat="i in [1, 2, 3, 4]">
  <td>i = {{ m = k; k = j; j = i + 1; i}}</td>
  <td>j = {{j}}</td>
  <td>m = {{m}}</td>
</tr>

I'm obtaining:

i = 1   j = 2   m =
i = 2   j = 3   m =
i = 3   j = 4   m =
i = 4   j = 5   m =

That is, m does not contain any value, despite the fact that it is bound to j (through k). It's possible it's because k itself is not evaluated.

My question is: isn't it a bug in AngularJS? Surely k should be evaluated if it's in the chain of bindings even though it is not displayed directly. Or do I misunderstand something?

I am aware that it is probably not an idiomatic way of using AngularJS, but I want to really understand the expression engine, and I can not explain this behaviour.

like image 846
Cyrille Ka Avatar asked Jul 08 '13 14:07

Cyrille Ka


1 Answers

There are a couple of issues here that are interacting.

First, your statements are ordered backwards: you're setting k=j before setting j, which leads to its being undefined.

Second, and more importantly, interpolated expressions (the ones in "{{}}") should not be used change the state of the scope. There's a good reason for this:

Interpolation and Watches

The way interpolation works is that when it compiles your html, angular registers a scope.$watch on each interpolated expression.

But these watched expressions can get executed multiple times during a digest: whenever a listener modifies the scope, it causes angular to run through the watches on that scope again. For this reason, watched expressions really ought to be "idempotent": i.e., they should have no side effects / cause no state changes. Here it is from the documentation for $watch:

The watchExpression is called on every call to $digest() and should return the value which will be watched. (Since $digest() reruns when it detects changes the watchExpression can execute multiple times per $digest() and should be idempotent.)

Tracing the Example

Specifically, here's what's going on in your example. First of all, it has nothing to do with the repeater. Each ng-repeat item gets its own scope, so what's going on here is equivalent to this simpler example:

<div ng-app>
    {{ i = 42 }}<br>
    i = {{ m = k; k = j; j = i+1; i }}<br>
    j = {{j}}<br>
    k = {{k}}<br>
    m = {{m}}<br>
</div>

(Here's the fiddle)

The digest proceeds like so:

  1. Evaluating {{ i = 42 }} sets scope.i to 42, and displays "42" in the document. However, since the scope has changed, there's ALSO a "dirty" flag set, which means we'll be looping through the watches again (see step 5 below)
  2. Evaluating {{ m = k; k = j; j = i+1; i }} sets scope.m and scope.k to undefined, and scope.j to 43, and displays "42" in the document.
  3. Evaluating {{ j }} displays "43" in the document.
  4. Evaluating {{ k }} and then {{ m }} both simply display "" in the document, since they're currently undefined.
  5. Now, since the dirty flag is set, we repeat all these watches; this time, when we run {{ m = k; k = j; j = i+1; i }}, k = j works because j exists; so k is assigned a value of 43.
  6. When we get down to {{ k }} this time, its value has changed from undefined to 2, so the dirty flag is set once again, but now the document is displaying "k = 2". However, {{ m }} is still undefined.
  7. Just like 5-6, we go through the watches again, and this time, {{ m = k; k = j; j = i+1; i }} results in m being set to 43.

Tangent: Double-processing Watches?

Tangentially, an interesting thing I encountered while tracing through the digest code: it seems that the dirty flag is ALWAYS set to true the first time through the watches, because a never-before-checked watch has no recorded "last" value. Seems like this would result in lots of unnecessary double-processing. This seems true even when the watched value is a constant like {{2}}. Am I misunderstanding something here?

like image 68
anandthakker Avatar answered Oct 23 '22 11:10

anandthakker