11

I have an Angular.js app that performs a large set of accounting-type calculations (revenue, costs, profit, etc) across a complex object model. A change to any part of the object model requires all of the calculations to be re-executed. Furthermore, many of the changes to the object model can be made directly from my templates using bindings - there's no need for a controller to intermediate.

Many of the calculation results need to both be displayed to the user and contribute to downstream calculations. For example, revenue is displayed to the user but is also used to calculate the profit (which is also displayed to the user). However, there's no point in calculating the revenue twice (it's a complex calculation) on each digest cycle, so an obvious optimization is to memoize when it is first calculated and then reuse this memoized value for the duration of the cycle.

The problem is that I need to clear these memoizations either at the beginning or end of the digest cycle. One approach would be for the controller to intercept every possible source of changes and manually unmemoize the object model before proceeding with the calculations. Instead, I'd much prefer to just keep using the bindings that have been set up.

As far as I can tell, what I need is a way to know when either the digest cycle is starting, or when it has finished. When I get this notification, I could reset the memoizations.

The Angular $watch documentation says:

If you want to be notified whenever $digest is called, you can register a watchExpression function with no listener. (Since watchExpression can execute multiple times per $digest cycle when a change is detected, be prepared for multiple calls to your listener.)

This is no use to me because clearing the memoizations on each iteration of the digest cycle would defeat the purpose of having them in the first place. I need only one notification per digest cycle.

How can I do this, or is there an alternate approach to solving the problem?

Ben Teese
  • 219
  • 3
  • 7
  • _"... I need to clear these memoizations ..."_ - Why? – Stewie Jan 06 '14 at 12:24
  • `is there an alternate approach to solving the problem` ... really not clear what actual problem is – charlietfl Jan 06 '14 at 13:08
  • What if you create a service to calculate this costs and etc and call the calculation method manually? – Deividi Cavarzan Jan 06 '14 at 13:51
  • @Stewie When the user changes something, a digest cycle will get triggered, and I want to either: 1. Clear the memoizations before starting the cycle in case the user's change would lead to different calculation results; or 2. Clear the memoizations after the cycle has finished, ready for the next cycle. Either way, I do not want the same memoizations to stay in place between digest cycles. – Ben Teese Jan 09 '14 at 09:23
  • @charlietfl I'll try and restate the problem: I'm memoizing method calls that I have bound to in my view, the goal being to optimize the watch expressions that get re-executed over multiple iterations of the watch list within single a digest cycle. However, because many different user interactions on the page could affect method results, I need to clear these memoizations whenever the user does something. Rather than having to explicitly intercept all possible user interactions in the controller and clear the memoizations then, is there a way to be notified about _any_ digest-cycle trigger? – Ben Teese Jan 09 '14 at 10:00
  • @DeividiCavarzan Thanks for the response Deividi. Unfortunately, if I push the calcs into a service the problem of knowing when to clear memoizations remains. – Ben Teese Jan 09 '14 at 10:05
  • Purpose of memoizations is to cache and serve the cached results when inputs are the same. So it's still unclear why would you need to explicitly clear the memoizations. You should really post some code showing what and how you're memoizing and why you need to reset. – Stewie Jan 09 '14 at 10:51
  • Well, if you you will clean the calcs in $scope.$destroy, you can attach a event listener for this and catch it up in the service. But if you didn't know the exact time that the calcs need to be cleaned, i really don't know a affective way to do this. – Deividi Cavarzan Jan 09 '14 at 18:40
  • About when it has finished processing, I solved it the approach) discussed in this post: http://stackoverflow.com/questions/22039251/how-to-trigger-a-callback-every-digest-cycle – Phil Thomas Mar 18 '14 at 15:48
  • Reactive programming is worth checking out if you want to propagate change in a graph. Take a look at http://www.ractivejs.org/ – Ehtesh Choudhury Oct 29 '14 at 03:35
  • It astounds me that this is not an obvious need for everyone using angular. I always end up with functions in my controller that will end up being called multiple times per digest and need their results cached and the cache invalidated at the beginning of the next digest. Why doesn't angular expose an API for this? – Ronnie Overby Apr 08 '16 at 14:52

1 Answers1

10

OK, I think I finally figured something out, so thought I'd answer my own question.

There's a private Angular method called $$postDigest that will callback a function after the end of the next digest cycle. However, it only runs once, and if you try to get it to reschedule itself recursively it'll go into an infinite loop.

To work around this, you can instead use $$postDigest in conjunction with a regular $watch. If you have a function called fn that you want a controller to invoke after every digest cycle, you'd add something like this to the controller function:

...
var hasRegistered = false;
$scope.$watch(function() {
  if (hasRegistered) return;
  hasRegistered = true
  // Note that we're using a private Angular method here (for now)
  $scope.$$postDigest(function() {
    hasRegistered = false;
    fn();
  });
});
...

This is based on a code fragment by Karl Seamon. Obviously it's not ideal to be calling a private method on $scope. Hopefully a $postDigestWatch method along these lines may eventually get added to Angular.

There's been some confusion in the comments on my original question regarding why you'd want to do this sort of thing: if you're still unclear, check out this blog post I wrote that covers my particular use-case.

Ben Teese
  • 219
  • 3
  • 7