Not blocking the UI in tight JavaScript loops

This is

as days pass by, by Stuart Langridge

. Here I write about many things. In the past I wrote about other things but the past is past. I write code for people to play with, I write about my life on Twitter, and I write here.

On Jul 03, 2009 I wrote Not blocking the UI in tight JavaScript loops, on the subject of JavaScript and the DOM.

Everyone's written a JavaScript loop that just loops over all the {LIs, links, divs} on a page*, and it's pretty standard. Something like
var lis = document.getElementsByTagName("li"); for (var i=0; i<lis.length; i++) { // yes this could be more efficient, don't care   // do something here to lis[i] };
or, if you're using jQuery:
$("li").each(function() {   // do something here to this });
This is problematic if there are, say, 2000 LI elements on the page, and what you're doing in the loop is semi-intensive (imagine you're creating a couple of extra elements to append to each of those LIs, or something like that). The reason this is a problem is that JavaScript is single-threaded. A tight loop like this hangs the browser until it's finished, you get the "this script has been running for a long time" dialog, and the user interface doesn't update while you're in this kind of loop. You might think: aha, this will take a long time, so I'll have some sort of a progress monitor thing:
var lis = document.getElementsByTagName("li"); for (var i=0; i<lis.length; i++) { // yes this could be more efficient, don't care   // do something here to lis[i]   progressMonitor.innerHTML = "processing list item " + i; // fail };
but that doesn't work. What happens is that the browser freezes until the loop finishes. Annoying, but there it is. One approach to getting around this is with timeouts rather than a for loop.
var lis = document.getElementsByTagName("li"); var counter = 0; function doWork() {   // do something here to lis[i]   counter += 1;   progressMonitor.innerHTML = "processing list item " + counter;   if (counter < lis.length) {     setTimeout(doWork, 1);   } }; setTimeout(doWork, 1); 
so you move the bit of work you need to do into a function, and that function re-schedules itself repeatedly, using setTimeout. This time, your user interface will indeed update, and your progress monitor will show where you're up to. There are a couple of caveats with this: it'll take a bit longer, and you're no longer guaranteed to have things processed in the order you expect, but they're minor issues. For doing this in jQuery, a tiny plugin:
jQuery.eachCallback = function(arr, process, callback) {     var cnt = 0;     function work() {         var item = arr[cnt];         process.apply(item);         callback.apply(item, [cnt]);         cnt += 1;         if (cnt < arr.length) {             setTimeout(work, 1);         }     }     setTimeout(work, 1); }; jQuery.fn.eachCallback = function(process, callback) {     var cnt = 0;     var jq = this;     function work() {         var item = jq.get(cnt);         process.apply(item);         callback.apply(item, [cnt]);         cnt += 1;         if (cnt < jq.length) {             setTimeout(work, 1);         }     }     setTimeout(work, 1); };
and now you can do
$.eachCallback(someArray, function() {   // "this" is the array item, just like $.each }, function(loopcount) {   // here you get to do some UI updating   // loopcount is how far into the loop you are });  $("li").eachCallback(function() {   // do something to this }, function(loopcount) {   // update the UI });
Not always a useful technique, but when you need it, you need it.
http://www.kryogenix.org/days/2009/07/03/not-blocking-the-ui-in-tight-javascript-loops
posted @ 2012-09-10 17:58  China Soft  阅读(165)  评论(0编辑  收藏  举报