11

It's better to write code that doesn't rely on the timing of immediate callbacks (like microtasks vs macrotasks), but let's put that aside for the moment.

setTimeout queues a macrotask, which, at a minimum, waits to start until all microtasks (and microtasks that they spawn) finish. Here's an example:

console.log('Macrotask queued');
setTimeout(function() {
  console.log('Macrotask running');
});
Promise.resolve()
  .then(function() {
    console.log('Microtask running');
  });
console.log('Microtask queued');
console.log('Last line of script');

The behavior of a .then on a resolved Promise is fundamentally different from the behavior of an immediate setTimeout callback - the Promise .then will run first, even if the setTimeout was queued first. But only modern browsers support Promises. How can the special functionality of a microtask be properly polyfilled if Promise doesn't exist?

If you try to imitate a .then's microtask by using setTimeout, you'll be queuing a macrotask, not a microtask, so the badly-polyfilled .then won't run at the right time if a macrotask is already queued.

There's a solution using MutationObserver, but it looks ugly, and isn't what MutationObserver is for. Also, MutationObserver is not supported on IE10 and earlier. If one wants to queue a microtask in an environment that doesn't natively support Promises, are there any better alternatives?

(I'm not actually trying to support IE10 - this is just a theoretical exercise on how microtasks can be queued without Promises)

Snow
  • 3,820
  • 3
  • 13
  • 39
  • 1
    I would suggest having a look at promise implementations that are performance-oriented, especially Bluebird. Taking a look at the [history of its `schedule.js`](https://github.com/petkaantonov/bluebird/commits/master/src/schedule.js) will be enlightening. – Bergi Jan 07 '20 at 23:27
  • Have you tried polyfiling the Promise using something like core-js? – Hugo Jan 13 '20 at 16:59

2 Answers2

8

I saw that mutationObserver callbacks use microtasks, and luckily, IE11 supports it, so I had the idea to queue a microtask in IE11 by saving the callback and then immediately triggering the observer by changing an element:

var weirdQueueMicrotask = (function() {
  var elementThatChanges = document.createElement('div');
  var callback;
  var bool = false;
  new MutationObserver(function() {
    callback();
  }).observe(elementThatChanges, { childList: true });
  return function(callbackParam) {
    callback = callbackParam;
    elementThatChanges.textContent = bool = !bool;
  };
})();

setTimeout(function() {
  console.log('Macrotask running');
});
console.log('Macrotask queued');
weirdQueueMicrotask(function() {
  console.log('Microtask running');
});
console.log('Microtask queued');
console.log('Last line of script');

You can open up IE11 and see the above working, but the code looks strange.

Snow
  • 3,820
  • 3
  • 13
  • 39
4

If we are talking about IE you can use setImmediate

https://developer.mozilla.org/en-US/docs/Web/API/Window/setImmediate

Also, MutationObserver is not supported on IE10 and earlier.

setImmediate is supported on IE10. So plus one IE version.
And, if your are interested, plus Node.js.

There's a solution using MutationObserver, but it looks ugly, and isn't what MutationObserver is for.

There are other possible polyfills, here is a couple of implementations: https://github.com/YuzuJS/setImmediate/blob/master/setImmediate.js (this one is mentioned in MDN) https://github.com/taylorhakes/setAsap/blob/master/setAsap.js (a more simple one)

And as almost all polyfills they are ugly as well.

But anyway, here is an example in its essence (using postMessage), and I think it is least ugly of all (but also not a true polyfill)

var setImmediate = (function() {
  var queue = [];

  function on_message(e) {
    if(e.data === "setImmediateMsg") queue.pop()()
  }

  if(window.addEventListener) { // IE9+
    window.addEventListener('message', on_message)
  } else { // IE8
    window.attachEvent('onmessage', on_message)
  }

  return function(fn) {
    queue.unshift(fn)
    window.postMessage("setImmediateMsg", "*")
  }
}())

setTimeout(function() {
  console.log('Macrotask running');
});
console.log('Macrotask queued');
setImmediate(function() {
  console.log('Microtask running');
});
console.log('Microtask queued');
console.log('Last line of script');
x00
  • 13,643
  • 3
  • 16
  • 40
  • Great finds, I like them all! – Snow Jan 11 '20 at 05:37
  • @Snow, by the way, you said it's a theoretical exercise, but, I'm still curious, how you came across this idea in 2019? – x00 Jan 15 '20 at 16:09
  • I was just wondering how microtasks could be queued, there really wasn't anything more specific. I was kind of hoping that there was *something* built into the language that provided access to them, other than Promises, but it looks like there isn't. All the other methods look to involve invoking environment-specific quirks that weren't designed for that sort of thing (but just *happen* to work anyway). – Snow Jan 15 '20 at 20:01