Web Development > Web Workers

  1. Overview
  2. Cross Browser Implementation
  3. Examples

Cross Browser Implementation

To allow people to start programming for web workers before they're implemented across all browsers what we need is a way to emulate their behaviour in browsers that don't currently support them. Obviously there's no way to achieve the multi-threading nature on the client if it's not natively supported, but we can create our own Worker object that behaves the same as the native one in all other respects. The way we do it is as follows...

First we check for native support of the Worker object. If it is not present then we create our own Worker class.

if(typeof Worker !== 'function')
{
    function Worker(sURL)
    {
        this.m_bReady = false;
        this.m_sWorkerVariableName = '$oWorker'; //variable name by which the loaded script refers to this object
        this.m_aoMessageQueue = [];
        this.m_sURL = sURL;
        this.m_asScripts = {};
        this.m_abScriptRequests = {};
        this._LoadScript(sURL);
    }

In this constructor we initialise various properties which we will need and call the _LoadScript method, which takes care of loading up all the necessary code from source sURL. We'll get to the implementation of this later. The worker object will also need onmessage and onerror methods, the user will override these functions to handle messages from the worker's code.

    Worker.prototype.onmessage = function(e)
    {
        //the worker's script postMessage(o) calls into this.onmessage({data: o})
        //override this to handle messages from the worker script
    };
 
    Worker.prototype.onerror = function(e)
    {
        //if the worker's script postMessage(o) throws an error, it will be caught and enter into here
        //override this to handle errors from the worker script
        //TODO - asynchronous events which cause an error in the 
        //       worker script will not trigger onerror to be called
    };

Our implementation will emulate messaging between this worker object and the worker source code with direct function calls. In more detail we're going to be re-writing the worker source code in such a way that wherever it would normally call postMessage it will now call a method directly on the worker object - let's call it _onmessage - which calls into the worker object's onmessage method.

    Worker.prototype._onmessage = function(oMessage)
    {
        this.onmessage({data: oMessage});
    };

Likewise, we need to handle calls to postMessage on the worker object itself. Calls to this method should trigger the onmessage handler in the worker source code. We handle this similarly to the previous case, this time we specify that postMessage on the worker object calls another method - let's call it _postMessage. When we rewrite the worker source code we replace assignments to the onmessage handler with an assignment to the worker objects _postMessage method instead.

    Worker.prototype._postMessage = function(e)
    {
    };
 
    Worker.prototype.postMessage = function(oMessage)
    {
        //passes messages to the worker script's self.onmessage function
        //if the worker script is not yet ready then add to a message queue
        if(this.m_bReady)
        {
            try
            {
                this._postMessage({data: oMessage});
            }
            catch(e)
            {
                this.onerror(e);
            }
        }
        else
        {
            this.m_aoMessageQueue.push(oMessage);
        }
    };
 
    Worker.prototype._FlushMessageQueue = function()
    {
        for(var nCursor = 0, nCount = this.m_aoMessageQueue.length; nCursor < nCount; nCursor++)
        {
            this.postMessage(this.m_aoMessageQueue[nCursor]);
        }
        this.m_aoMessageQueue.length = 0;
    };

Note here that we're checking if the Worker has fully loaded up all it's scripts with the property this.m_bReady. If the worker is not yet ready then any postMessage requests are added to a queue which can be flushed when loading is complete. So, on to the loading...

    Worker.prototype._LoadScript = function(sURL)
    {
        if((this.m_abScriptRequests[sURL] !== true) && 
           (this.m_asScripts[sURL] == null))
        {
            var pMe = this;
            this.m_abScriptRequests[sURL] = true;
            XHR.LoadData(sURL, true, function(sScript){pMe._OnLoadScript(sURL, sScript);}, XHR.TEXT);
        }
    };

The _LoadScript method will, so long as the script hasn't already been requested, make an XMLHttpRequest for the text located at sURL. I'm using my own object XHR that wraps up all the XMLHttpRequest functionality for convenience, you can implement this however you desire. When the response is recieved the string is passed to the _OnLoadScript method for processing.

    Worker.prototype._OnLoadScript = function(sURL, sScript)
    {
        //find all scripts that need importing and load them
        //TODO - allow the script to specify the name of the file dynamically
        var pMe = this;
        var reImportScripts = /importScripts\(['|"](.*)["|']\)/g;
        var fnImportReplacer = function(sMatch, sURL, nOffset, sScript)
        {
            pMe._LoadScript(sURL);
            return sMatch;
        }
        sScript = sScript.replace(reImportScripts, fnImportReplacer);
 
        //replace the worker script's postMessage method with a direct call to the worker's onmessage handler 
        //(script.postMessage triggers worker.onmessage)
        sScript = sScript.replace(/(self\.)?postMessage/g, this.m_sWorkerVariableName + '._onmessage');
 
        //replace the workers script's onmessage handler with the worker's _postMessage method 
        //(worker.postMessage triggers script.onmessage)
        //note, make sure we don't replace the _onmessage that we just added by checking the _
        sScript = sScript.replace(/[^_](self\.)?onmessage/g, this.m_sWorkerVariableName + '._postMessage');
 
        delete this.m_abScriptRequests[sURL];
        this.m_asScripts[sURL] = sScript;
 
        for(var sKey in this.m_abScriptRequests)
        {
            //still have pending requests
            return;
        }
 
        //only get here when have finished
        this.m_abProcessedImport = [];
        var sScript = this._ProcessImport(this.m_sURL);
 
        var fn = new Function(
            this.m_sWorkerVariableName, 
            'try{' + sScript + '}catch(e){' + this.m_sWorkerVariableName  + '.onerror(e);}'
        );
        fn(this);
 
        this.m_bReady = true;
        this._FlushMessageQueue();
    };
 
    Worker.prototype._ProcessImport = function(sURL)
    {
        if(this.m_abProcessedImport[sURL] !== true)
        {
            var pMe = this;
            var sScript = this.m_asScripts[sURL];
 
            //TODO - allow the script to specify the name of the file dynamically
            var reImportScripts = /importScripts\(['|"](.*)["|']\)/g;
            var fnImportReplacer = function(sMatch, sURL, nOffset, sScript)
            {
                return pMe._ProcessImport(sURL);
            }
            sScript = sScript.replace(reImportScripts, fnImportReplacer);
 
            this.m_asScripts[sURL] = sScript;
            this.m_abProcessedImport[sURL] = true;
        }
        return this.m_asScripts[sURL];
    };
}

Let's step through this. Firstly we need to deal with any scripts that are imported into the script we're loading. We do this by using a regular expression to find all calls to importScripts, extract the script to be loaded and call _LoadScript for it. We leave these importScripts calls in place for later and move on.

The next step is to rewrite any postMessage calls in this worker script, again using a regular expression. This will, for example replace something like postMessage("hello") with $oWorker._onmessage("hello").

Similarly we rewrite the onmessage handler of this worker script. For example replacing onmessage = function(e){} with $oWorker._postMessage = function(e){}.

Next we check if this is the final script we need to load (remember the importScripts calls triggered further loading). If it is we continue to process all the imported scripts. We start from the originally requested file and recursively call the _ProcessImport method which replaces the importScripts code with the actual script itself.

So, now the processing of the script text is complete we use it to create a function fn. The code for this function is the script text we just created. We also specify that the function takes one argument, called $oWorker, which explains our previous replacements using the $oWorker variable. Finally we call this function passing this as the $oWorker argument, which hooks up the worker script to this worker object. That's it, magic!

There are a couple of things still to do to improve this. Firstly an importScripts call must take a single literal string as its argument, if there are dynamically determined arguments, or if there is more than one argument, then the import will not be handled. Also any script errors that occur in the worker script that were initiated asynchronously will not be caught by the onerror handler. But despite these it's fairly functional.

So let's see this in use...