Thread safe DOM manipulation

From Trephine

Jump to: navigation, search
« Site improvements - CSS sprites Implementing JavaScript worker threads »

[subscribe] Recent blog entries

Live Demos

Thread safe DOM manipulation

In the article trephine worker threads made easy, I explored a few mechanisms which make launching anonymous worker threads trivial by altering the Function prototype. This article expands on those concepts, developing simple ways to create Java worker threads that can safely interact with the browser DOM.

Recall that we had come up with this method for adding a run() method to trephine/Rhino JavaScript functions:

Function.prototype.run = function run() {
  var self = this, args = [];
  for (var i=0, l=arguments.length; i<l; i++) args[i] = arguments[i];
  var thread = new java.lang.Thread(
    new java.lang.Runnable({
      run: function() {
        self.apply(null, args);
      }
    })
  );
  thread.start();
  return thread;
};

And that the usage pattern was this:

function sleepMessage( message, duration ) {
  var thread = java.lang.Thread.currentThread();
  thread.sleep( duration );
  java.lang.System.out.println( message );
}
var sleepThread = sleepMessage.run( "What are you McFly, chicken?", 1000 );

The above code defines the sleepMessage() function, then calls it as the body of a new anonymous worker thread, passing in some values.

Now, worker threads are aren't very useful unless they actually do something. In the previous example, the sleepMessage function merely writes a message out to standard output, but say we wanted to manipulate the DOM in some way instead.

As we explored previously, this can be accomplished using window.eval() and passing in a string containing code to execute. This is cumbersome because it means we have to encode both our actual executable JavaScript and any data passed in as a string.

One solution is to again alter the Function prototype in the trephine/Rhino context, this time adding an eval() method which will cause the function to execute in the DOM JavaScript environment:

Function.prototype.eval = function eval() {
  var args = [];
  for (var i=0, l=arguments.length; i<l; i++) args[i] = arguments[i];
  var code = [ '(', this, ').apply(null,', JSON.stringify(args), ')' ].join('');
  return window.eval( code );
};

When called, eval() will collect its arguments and format a string to be passed to window.eval. In this context, the window object is a JSObject instance referring to the DOM window. The JSON object is provided by trehpine and is a trimmed down version of Douglas Crawford's json2.js implementation.

Recall that calling window.eval directly triggers a deadlock as the browser's JavaScript execution thread and the Java applet's main thread each wait on results from the other. So if we want to use a function's new eval() capability, it will have to be within a worker thread. The following example shows how to do this:

function sleepAlert( message, duration ) {
  var thread = java.lang.Thread.currentThread();
  thread.sleep( duration );
  function domCallback( msg ) {
    alert( msg );
  }
  domCallback.eval( message );
}
sleepAlert.run( "Nobody calls me chicken.", 1000 );

Just like the sleepMessage function defined earlier, sleepAlert is called as the body of a worker thread by invoking its run() method. Inside the function, it sleeps for the provided duration, and then the inner domCallback function is invoked through eval(), causing its internals to be executed in the DOM JavaScript environment.

This may seem a little complicated, but it's much less coding than writing out all that Thread subclassing and eval string compaction by hand each time. Also, we can trim it down by using anonymous functions rather than named functions. Here's the same example, but without naming all the various worker functions and variables:

(function( message, duration ) {
  java.lang.Thread.currentThread().sleep( duration );
  (function( msg ) { alert( msg ); }).eval( message );
}).run( "Nobody calls me chicken.", 1000 );

Another option to further simplify the experience is to combine eval and run so that from the trephine/Rhino context there is a single gesture for executing DOM code in a thread safe way:

Function.prototype.runeval = function runeval() {
  var self = this, args = [];
  for (var i=0, l=arguments.length; i<l; i++) args[i] = arguments[i];
  return Function.prototype.run.call( function( args ){
    Function.prototype.eval.apply( self, args );
  }, args );
};

Here we've defined the runeval() method which does the following:

  • Collects any arguments passed in,
  • Defines an anonymous funciton which internally calls eval(),
  • Kicks off a worker thread from the anonymous function using run(),
  • Passes forward all arguments through the above steps.

The result is that now we have a simple mechanism for calling DOM JavaScript in a safe way from the trephine/Rhino context:

(function( msg ){ alert( msg ); }).runeval( "Nobody calls me chicken." );

Whew. I hope all that made sense. Event-driven development its tricky enough to get right, even without introducing the complexity of thread management and context traversal. That said, I believe that the forthcoming wave of ultra-rich Internet applications will demand resources in excess of what can be accomplished using traditional browser capabilities, and so understanding when and how to employ threaded techniques will be a crucial differentiator between the masters and the masses.

Note that the snippets and examples in this article target the Rhino/Java environment, and so need to be called through trephine.js to be effective. In an upcoming article, I'll discuss making similar changes to the browser JavaScript environment so that worker threads are easy to create everywhere.

As always, I look forward to your questions, suggestions and comments!

Public domain declaration

Just so there's no confusion: all of the code snippets on this page are provided "AS IS", without warranty of any kind, express or implied.

All of the code snippets on this page are hereby released into the public domain by the me, the copyright holder. This applies worldwide. Or in case this is not legally possible: The copyright holder grants any entity the right to use this work for any purpose, without any conditions, unless such conditions are required by law.

If you'd feel better with a "real" license, you're free to use code snippets on this page under the MIT license as described on the about page.

Any links back to this site are always appreciated, but not required. Enjoy!

--Jim R. Wilson (jimbojw) 21:11, 24 April 2009 (UTC)