4

Given:

  • some sort of widget based web app
  • lots of JS functionality
  • high coupling (communication/callbacks between widgets)
  • widgets draw themselves
  • certain widgets need to do a complete and fairly expensive redraw every time one of their dependencies is changed.

Problem: there can be complicated dependency chains and it becomes difficult to make sure redraws only happen once per user action.

For example:

  • A needs to be redrawn if B or C change
  • B is dependent on C's state
  • So if C changes, so does B and both may end up causing A to redraw.

If this only happens a few times, it's easy to solve. But when it happens all over, it becomes a nightmare to engineer and makes maintenance even worse.

I assume this is a somewhat common problem, so there must be some method of dealing with. That or my designs are flawed in some way.

Any suggestions? Either some design pattern or an algorithm to deal with limiting redraws would be very much appreciated.

Edit

Alright, based on Randall's suggestions, here's what I'm thinking:

  • Semaphore like global variable but without any multi-threading stuff so basically just a global count. Let's name this S. I would have one per widget that requires expensive redraws.
  • S has a signal and a wait function. Signal increments S and wait decrements it.
  • S is initialized with a callback function to the draw of the widget.
  • Every time something is triggered (user event, ajax return, etc) S.signal() is called.
  • Once the event is finished changing things within an object (after callbacks are made), S.wait() is called.
  • S.wait(), after decrementing S check if count is 0. If it is, it redraws.

So if C changes:

  • C runs S.signal() (S==1)
  • C makes its changes, callback to B
  • B runs S.signal() (S==2)
  • B makes its changes
  • B runs S.wait() (S==1, no redraw)
  • B finishes, C starts again.
  • C runs S.wait() (S==0, redraw)

Like the dirty flag but more of a dirty-count and I think this is easier to implement. It can be extended for additional functionality fairly easy. If, for some reason, I want to optimize it so not everything is redrawn in certain cases, I can add some flags that represent things to be redrawn and reset them every draw.

Also, it could be replaced with a stack and do basically the same thing but with some better extension capabilities.

Edit 2

Izkata's solution turned out to be trivial to implement and only slightly less flexible. Only 2 changes are needed:

  • Add a timerID property to the widget that needs redrawn.
  • Wrap redraw function in a timer:

JS code:

A.draw = function(){
    var that = this; 
    clearTimeout(this.drawTimer);
    this.drawTimer = setTimeout(function(){that._draw();},0);
}
A._draw = function(){
    //old draw code goes here
    //could be made "private" or nested in A.draw
}
  • No other changes are needed so your interface stays the same and it works unless you need it to work with Async code like ajax calls.
  • Is this an observed problem? Or a theoretical one? – svidgen Aug 14 '14 at 20:34
  • Also, what do you mean by draw? Are your widgets actually performing their own drawing calculations and manipulating a canvas or SVG? Or, are we talking about changes to the DOM? – svidgen Aug 14 '14 at 20:39
  • @svidgen Observed and it keeps becoming more apparent the more complex projects get. And by draw I mean changes to the dom and the calculations required before hand. The changes often require basically deleting the widget and adding it again though. – slicedtoad Aug 15 '14 at 13:10
3

Good question. I think you need to separate capture of the need for a redraw from the act of redrawing. Basically, as dependent things change, they notify their parent. The parent then marks itself as being "dirty" which means it needs a redraw. At a later time, the redraw occurs and the parent clears its "dirty" flag. This is common in event-driven windowing and UI systems (sorry, I can't remember a google-able name for this right now).

In your case, if the dirty flag is going from clear to set, you might want to schedule a redraw operation for a short time in the future. Then, other events can occur which will trigger the dirty flag to be set multiple times. After time has passed, you can redraw what needs it. Hopefully the wait time can be short enough to not be noticeable to the user, but long enough to collect all the likely changes that need to be redrawn.

An alternative to the time-based approach proposed above is to use logic to somehow detect that the system has stabilized and defer redraws until that point. I'm not sure what triggers the changes that necessitate the redraws, but perhaps there is an obvious point where the changes have all been made.

A third approach might be to have your system use an event queue to drive its work. The queue could be prioritized, such that data modification events come before redraw events, and that there need be only a single redraw event per widget (or a single event for the entire page).

Since SO sometimes collapses comments, people should read Izkata's suggestion below for using a zero-delay timeout and JavaScript's single-threaded nature to create what is effectively an ad hoc queue to ensure that the redraws get done after the main processing. Clever.

  • hmm, that might work, let me see if I can think of a way to handle it without timers. And if anyone knows of some name for this 'dirty/clean flag' process, please comment. – slicedtoad Aug 14 '14 at 20:19
  • I added another approach: an event queue. I'll add more as ideas pop into my head. – Randall Cook Aug 14 '14 at 20:30
  • 2
    @slicedtoad Javascript is, for the most part, single-threaded. Assuming B and C don't delay any processing themselves, this means you can (ab)use setTimeout with a timeout of 0 and not even need a dirty flag - whenever A determines it needs to redraw, put the redraw behind the timeout and cancel the previous one with clearTimeout. – Izkata Aug 15 '14 at 3:18
  • @Izkata I'm somewhat unclear on how that would work, wouldn't A, B and C need to be running as separate processes for that? If C is changed, and runs a callback to B, this callback is just executing a function in sequential order, so a timeout won't do anything since there aren't other tasks waiting. Maybe if I was using a signalling or custom event listeners... Or does timeout not work like that. – slicedtoad Aug 15 '14 at 13:25
  • 1
    @slicedtoad That's not how they work in Javascript. Because it's single threaded, anything setTimeout triggers will happen after the current callbacks - it won't interrupt the current execution flow. B updates A, A calls setTimeout(), B finishes execution, A's timeout callback triggers. Or if B and C both ran immediately, the update to A will happen twice before any setTimeout callback can trigger, and the second update will cancel the first timeout – Izkata Aug 15 '14 at 13:58

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.