Search code examples
javascriptgoogle-chromeweb-worker

Why does using a function declaration not work for onmessage with web workers?


The TL;DR is that when the onmessage() handler for a web worker is defined as a function declaration as opposed to a function expression, it doesn't work. (At least in Chrome.) And I can't for the life of me think of why.

I have the following dirt-simple example setup:

index.html

<!DOCTYPE html>
<html>
<head>
  <title>Web worker test</title>
  <script src="index.js"></script>
</head>
<body>
  <input type="button" value="test1" onclick="run('test1.js')">
  <input type="button" value="test2" onclick="run('test2.js')">
</body>
</html>

index.js

function run(filename) {
  worker = new Worker(filename);
  worker.postMessage("test");
}

test1.js

onmessage = function(msg) {
    console.log(msg.data);
}
console.log(self.onmessage);

test2.js

function onmessage(msg) {
    console.log(msg.data);
}
console.log(self.onmessage);

You can try this yourself at http://mrbean.cryogenia.com/~davedude/webworkertest/ . If you click the test1 button, it logs the following (on Chrome 79.0.3945.117 at least):

ƒ (msg) {
  console.log(msg.data);
}
test1.js:2 test

This shows that the function is defined as expected, and the handler is working.

In contrast, when you click test2, all you get is this:

ƒ onmessage(msg) {
  console.log(msg.data);
}

This shows the script is working, and the function is defined, it's just not functioning as a handler for whatever reason.


Solution

  • The global object's onmessage property is a setter. Setters do not get invoked by function declarations; rather, they simply get overwritten, as if delete <fnName> was run beforehand. Here's another example:

    <script>
    Object.defineProperty(window, 'fn', {
      set(newVal) {
        console.log('setter invoked');
      },
      configurable: true
    });
    </script>
    <script>
    function fn() {
    }
    
    console.log(window.fn);
    </script>

    As you can see, the setter does not get invoked. The state of the fn property on window after the first <script> is nearly the same as the state of the message property on self in the web worker at the start of the web worker's script:

    const workerFn = () => {
      console.log(Object.getOwnPropertyDescriptor(self, 'onmessage'));
    };
    const workerFnStr = `(${workerFn})();`;
    const blob = new Blob([workerFnStr], { type: 'text/javascript' });
    const worker = new Worker(window.URL.createObjectURL(blob));
    <div>Look at results in your browser console, not the snippet console</div>
    
    <br><br>
    
    <img src="https://i.sstatic.net/8JND8.png">

    The functionality of setting the function as a listener for the message event will only occur inside the setter. But without the setter being invoked, the listener will not be attached.


    If you're curious, in the specification, when a function declaration on the top level exists, and that name already exists on the global object, that name will not be re-created or re-initialized until, at the very end:

    Perform ! varEnvRec.SetMutableBinding(fn, fo, false).

    This will throw an error only if the binding is immutable (eg, if the property is not configurable). But the onmessage getter/setter is configurable.