Web Workers are a valuable addition to JavaScript, but their usage can feel unconventional. Imagine them as individual execution units, akin to single-threaded thread pools, communicating through messages. This differs from traditional thread interactions where a function is directly passed to a thread for execution.
The .NET Task.Run method provides an intuitive interface for this purpose. It allows you to execute a function within a thread pool and seamlessly chain subsequent code using async/await.
Let’s explore how to achieve a similar pattern in JavaScript. The goal is to have a Web Worker receive a function for execution and return a Promise that can be awaited.
Here’s how we can achieve this:
- Create a Promise.
- Create a Web Worker and send it a function (serialized as a string) for execution using
postMessage. - Utilize
eval within the Worker to execute the received function. - Resolve or reject the Promise based on the function’s execution result within the Worker.
The core Web Worker code resides in a separate file (“WebWorkerCode.js”) and is straightforward:
1
2
3
4
5
6
7
8
9
10
11
12
| // e is an array with 2 elements: the function in string format and an array with the arguments to the function
onmessage = function(e){
let fnSt = e.data[0];
let args = e.data[1];
//we need the () trick for eval to return the function
let fn = eval("(" + fnSt + ")");
let workerResult = fn(...args);
console.log('Posting result back to main script: ' + workerResult);
postMessage(workerResult);
}
|
We introduce a PromiseWorkerHelper class with a run method. The constructor initializes a Worker. The run method generates a Promise, captures its resolve/reject handlers, and sends the function to the Worker. Upon function execution completion, the Worker posts the result, prompting the PromiseWorkerHelper to resolve the Promise.
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
| class PromiseWorkerHelper{
constructor(){
//one shot object, we create it and invoke its run method just once
this.alreadyCalled = false;
//this.worker = new Worker("WebWorkerCode.js");
this.worker = this._createWorkerFromString();
//this is executed when the worker posts a message
this.worker.onmessage = (msg) => this.resolveFunc(msg.data);
this.worker.onerror = (msg) => this.rejectFunc(msg);
this.resolveFunc = undefined;
this.rejectFunc = undefined;
}
run(fn, ...args){
if (this.alreadyCalled){
throw "already used once";
}
this.alreadyCalled = true;
let pr = new Promise((resolve, reject) => {
this.resolveFunc = resolve;
this.rejectFunc = reject;
});
this.worker.postMessage([fn.toString(), args]);
return pr;
}
_createWorkerFromString(){
let workerOnmessageHandler = function(e){
let fnSt = e.data[0];
let args = e.data[1];
//we need the () trick for eval to return the function
let fn = eval("(" + fnSt + ")");
let workerResult = fn(...args);
console.log('Posting result back to main script: ' + workerResult);
postMessage(workerResult);
};
let str = "onmessage = " + workerOnmessageHandler.toString() + ";";
let blob = new Blob([str], {type: 'application/javascript'});
return new Worker(URL.createObjectURL(blob));
}
}
|
Instead of a separate file, the _createWorkerFromString method allows us to embed the Worker code directly, creating a Blob and generating a URL for the Worker constructor.
Here’s how you can use this implementation:
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
| function longFormatting(txt, formatCharacter){
console.log("starting longFormatting");
let res = formatCharacter + txt + formatCharacter;
start = Date.now();
while(Date.now() - start < 1000){}
console.log("finishing longFormatting");
return res;
}
function main(){
document.getElementById("launchCalculationBt").addEventListener("click", async () => {
console.log("Main.js, inside button handler");
let txt = "hello";
let formatCharacter = "---";
let promise = new PromiseWorkerHelper().run(longFormatting, txt, formatCharacter);
let res = await promise;
console.log("Main.js, result: " + res);
});
}
window.addEventListener("load", main, false);
|
The code is available in a gist and can be tested from here. Observe the console output for results.
Note that unlike .NET’s Task.Run, which accepts only the function, here we pass both the function and its arguments to run. In JavaScript, since we serialize the function into a string, closures aren’t feasible for passing parameters.
Web Workers
[Task.Run](https://msdn.microsoft.com/en-us/library/system.threading.tasks.task.run(v=vs.110)
here