Web workers are an immensely useful tool, enabling concurrency and perhaps parallelism with JavaScript. Before their introduction, any non-trivial computation would completely lock up the user interface of the current document, or even the entire browser.
One could work around this by breaking up a large job into very small parts and chain them with timers, but this is highly inefficient and choosing an ideal amount of work for each part across all clients is almost impossible.
However, the interface for creating workers is tricky; the Worker
constructor takes an external script's filename to execute.
Suppose you are developing a JavaScript library that relies upon workers
heavily. Are you forced to have two separate script files? No!
Interestingly, it is possible to have a single JavaScript file act as both a parent and a worker, but the implementation is not completely intuitive.
First of all, the single-file library must be aware of whether it is the
parent or a worker. This can be achieved by detecting the presence of the DOM
document
:
var is_worker = !this.document;
Of course, if you need to execute the above in a context where this
isn't the global object, then you can reliably obtain the global
with something like
var global = (function(){ return this; })();
Surely, spawning a worker from the parent is now as simple as this, right?
var worker = new Worker('mylibrary.js');
Not quite. Worker paths are not resolved relative to the parent script file's path, but instead, relative to the path of the parent page. If the script isn't in the same directory as the page, the above will fail. Also, if the user has renamed the library file, workers will break.
The path of the script relative to the page must be used instead. This can be
obtained by appending a dummy element with document.write
and
getting the previous element's src
. The previous element, of
course, is the script
tag of the parent script, as conveniently for
this situation, scripts block the building of the DOM tree while running.
var script_path = is_worker ? null : (function() { var id = +new Date + Math.random(); document.write('<script id="dummy' + id + '"><\/script>'); return document.getElementById('dummy' + id). previousSibling.src; })();
The test for is_worker
is present because DOM manipulation can
only be done when the script isn't running as a worker. Thankfully, only the
parent script needs to know the path to start workers, unless you want to start
subworkers, but the method to do that already resolves paths relative to the
worker's location.
A <script>
tag is used as the dummy element because it's
guaranteed to be a valid child element, be it <head>
or <body>
, unlike context-sensitive elements such as <div>
. It's also not an element that might have side-effects on
page display depending on CSS.
Now, workers can be spawned in the script with
var worker = new Worker(script_path);
The library in a single file might look something like this:
(function(global) { var is_worker = !this.document; var script_path = is_worker ? null : (function() { var id = +new Date + Math.random(); document.write('<script id="dummy' + id + '"><\/script>'); return document.getElementById('dummy' + id). previousSibling.src; })(); function msg_from_parent(e) { // event handler for parent -> worker messages } function msg_from_worker(e) { // event handler for worker -> parent messages } function new_worker() { var w = new Worker(script_path); w.addEventListener('message', msg_from_worker, false); return w; } if (is_worker) global.addEventListener('message', msg_from_parent, false); // the rest of the library goes here // to spawn a worker, use new_worker() })(this);