It’s been a week since I gave a talk about Angular.js and someone mentioned the importance of not calling $apply during a $digest cycle. I have read this too, but it struck me at the time that this could even be possible given that script execution in the browser is single threaded. This single threaded-ness is also the principle behind Node.js and it’s asynchronous architecture. So I decided to follow up on my own.
The Browser Update Cycle
A browser splits it time between two phases. The first is rendering the page by reading the markup and applying styles. The second is to execute any scripts that have been queued to run. Then it renders any changes to the page and executes the next script that has been queued. This is actually where the practice of using
to allow UI updates comes from. This creates a time out with no delay and fn() is pushed on the end of the script queue. Once the current script scope, the entirety of the call stack, is finished the script engine yields to the render cycle and in the next script cycle fn() is called. If you use Underscore.js (_) the short hand function is _.defer(fn()). Node.js does the same thing though with out the render cycle and because all interaction outside of the main thread is pushed you callbacks the process never waits, and efficiently processes the next function that was queued.
Thread Blocking and Preemption
The rules for execution is that the queue takes functions and each function is executed to its completion. In this process there is no preemption in the execution of code. No matter how deep the call stack may be when a time out or event fires it is put on the end of the script queue and serviced in order of arrival. In this way a complex or poorly written loop can block the execution of the code that is in the queue, and essentially blocks execution.
The issue with $apply and $digest
So with the theory of what we know, how is it possible to call an $apply during a $digest? Angular has it’s own event loop, and one of the things it does is observes any changes to the data in the $apply cycle. This happens automatically for models that are updated inside controllers, scopes, or ng- events, and manually by calling $apply directly. The next step is for angular to call $digest to apply these changes to the scope. Now from what know $apply can’t preempt or interrupt the execution of $digest so what cause the error. The issue is that Angular knows that $digest has been put on the process queue and that calling $apply will interfere with cycle. Angular tracks this by setting the $$phase in the root scope.
Theory is sound but I feel better when I have examples that prove it. So I created two examples to illustrate the point of blocking and the absence of preemption.
This code shows that the interval wont fire until it has a chance to execute after the current block of code has finished. It is interesting to note that not only do only just one of the intervals fire but if the clearInterval() is called in the first code block and not from a timeout the interval never gets a chance to fire at all.
This example shows that no matter where the time out is set that they will not execute until after the current code block and call stack is finished. The timeouts also execute in order of their placement in the queue, as any proper queue should function. :p