[EDIT: Dec, 2012] I once again revisited this problem with a recent project and rewrote the code below into a (much more advanced) requireJS module, adding things like onSuccess and onError event handling for the Queue. You can find the code here. I’ll be using it in the Data Generator 3.0.0 rewrite [https://github.com/benkeen/generatedata].
https://gist.github.com/4242808
I’ve lost count of the number of times this problem has caused me headaches. So this last time I decided to devise a generic solution – a javascript execution queue – that should work for all similar cases. Read below for an discussion of the problem and solution. I found a number of other solutions on the web, but none that fit well enough for my case. So here’s my entry to the genre…!
The Problem
When you use innerHTML to insert some content into a webpage, you cannot instantly access the content via javascript. You have to wait several milliseconds – only then can the elements be accessed.
If several hundred forum posts are anything to go by, people generally rely on the trusty setTimeout function to get around this problem, but every time I did this it really stuck in my craw. This was for two reasons:
- Using a timeout involves guesswork: are you sure that timeout’s long enough to ensure the code can now safely execute? How about those cases where people’s browsers are busy doing other things, or are just plain slow?
- I didn’t understand why it was even necessary. It was as though this phenomenon broke the sequential nature of javascript: surely a line of code would only get executed after the previous line had finished executing?
Well, to answer #2 for similarly bewildered people: statements DO only get executed after the previous statement has been completed. The confusion lies in the fact that all modern browsers implement innerHTML by converting the raw string into DOM nodes after it’s been inserted – and this takes time. So while the initial statement of setting the innerHTML has been completed, the content is still not accessible for a short period of time.
I recently encountered a situation that no judicial use of timeouts could solve. The problem was with the load mechanism for my Data Generator script. When a user is logged in and wishes to load a saved form, it uses Ajax to pull from the server the entire form content stored in a JSON object, then populate the form, row by row. This can potentially take large amounts of processing time – even several seconds, depending on the size of the form. In this situation, there were so many page elements being shifted around, it was simply too complex to get right with multiple setTimeouts.
An aside about innerHTML vs. DOM / A disclaimer for why I used innerHTML
Building and manipulation DOM nodes is elegant, controlled and standardized. Reason enough to take that approach, you’d think. But I do believe some situations call for innerHTML. Here’s a few things to keep in mind:
- innerHTML is faster than DOM manipulation, both for processing time and development time,
- innerHTML is simpler to use and therefore less prone to errors than DOM manipulation,
- using innerHTML over the DOM can result in (potentially significantly) less JS code to be downloaded by the client browser,
- innerHTML contents after insertion are, on all major browsers, converted to DOM nodes and therefore normally accessible through JS (eventually…),
- innerHTML, even though non-standard, is implemented on all major browsers (albeit with minor, frustrating differences).
In the case of the Data Generator, I had large chunks of HTML for each data type available to be duplicated for any particular data row at any time. For example, if the user chose “State / Province”, the HTML that needed to be inserted into the row totalled 50 lines and maybe 100 DOM nodes (not to mention attributes). Now imagine building that with the DOM…! Instead of 50 lines of HTML and one innerHTML call, we have 200 lines of dense javascript. In this case innerHTML was a simpler, faster, solution with less javascript to be downloaded by site visitors.
The Solution
Okay, back on topic.
So to solve this, I implemented a separate javascript queue layer, which would enforce the sequential execution of statements – even for this situation with innerHTMLs timing problems. Instead of accepting that a statement is complete when it normally finishes it’s execution, I’d define my own custom boolean test to determine this status. (For anyone that’s done much actionscript, this technique is probably quite familiar for queuing your events). Here’s the code: as you can see, it’s quite simple.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | /* [0] : code to execute - (function) [1] : boolean test to determine completion - (function) [2] : interval ID (managed internally by script) - (integer) */ var g_queue = new Array(); function process_queue() { if (!g_queue.length) return; // if this code hasn't begun being executed, start 'er up if (!g_queue[0][2]) { // run the code g_queue[0][0](); timeout_id = window.setInterval("check_queue_item_complete()", 50); g_queue[0][2] = timeout_id; } } function check_queue_item_complete() { if (g_queue[0][1]()) { window.clearInterval(g_queue[0][2]); g_queue.shift(); process_queue(); } } |
There’s a single global array: g_queue which stores the actions to queue. The first two elements are function objects. I chose that particular data structure because it provided an elegant way to pass in groups of statements, not just a single line of code, which wasn’t sufficient in my case.
The overall process is pretty straightforward: it executes the content of the first index ONCE, then puts the boolean test (the second index) in a setInterval call, to be executed however often you want (here, it’s set to 50 milliseconds). Only when the boolean test returns true does it remove the entry from the queue and start processing the next code in line. If there’s nothing in the queue, it just returns.
Here’s how you’d add an item to the queue.
1 2 3 4 5 6 7 8 9 | g_queue.push( [ function() { // here's the code that takes TIME (e.g. innerHTML statements). These statements only get executed once. }, function() { // this is our boolean test (e.g. if ID inside the innerHTMLed content exists). // It would return TRUE when the code in the previous function has been fully executed. } ] ); |
Then, once you’ve added one or more items, just call the process_queue() function to sequentially execute the statements. And bingo! “True” sequential processing even for innerHTML calls.
A final note on javascript closures: I believe using closures like in the anonymous function() definitions above don’t cause memory leaks in of themselves, since there are no circular references and the browser should be able to garbage collect the useless function after they’ve been executed. You can push entries into the queue in any execution scope. Please correct me if I’m wrong about this, though.
I very much hope this solution comes in handy!
Here’s a MUCH better solution.
http://domscripting.com/blog/display/99
Thanks, this helped a lot!
Works for me!
Thanks!
As for your “better solution” there are some cases that the domscripting example does not apply.
For example.
My application generates a report that has quite a few output tables with hundreds of rows with tens of cells fancy sorting, etc, etc.
This can cause real heartache with IE trying to render all those tables at the same time.
My solution was to write those tables to hidden the value of hidden fields and then sequentially load div’s with the table code.
This allows IE to focus on one table at a time and keeps it from crashing under the load.
Your g_queue solution worked perfectly and I am a very happy man.
thanks again
-Scott
Thanks for the post Scott – and I’m glad it worked out! Sounded like a neat problem.
I’ve heard of other problems with that other approach too, but never confirmed them. But the opportunity just arose: I’ve started working on an Adobe AIR version of the script that uses the queue approach above, but AIR prevents the use of eval() for security reasons. Should be interesting to see if the DOM approach works instead…!
- Ben