最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - How does the following queueMicrotask polyfill fallback to using setTimeout? - Stack Overflow

programmeradmin2浏览0评论

Consider the following polyfill for queueMicrotask.

if (typeof window.queueMicrotask !== "function") {
  window.queueMicrotask = function (callback) {
    Promise.resolve()
      .then(callback)
      .catch(e => setTimeout(() => { throw e; }));
  };
}

The description on MDN states.

It creates a microtask by using a promise that resolves immediately, falling back to using a timeout if the promise can't be created.

The queue-microtask library also uses the same polyfill. Here's what its documentation says.

  • Optimal performance in all modern environments.
    • Use queueMicrotask in modern environments (optimal)
    • Fallback to Promise.resolve().then(fn) in Node.js 10 and earlier, and old browsers (optimal)
    • Fallback to setTimeout in JS environments without Promise (slow)

This raises more questions than answers.

  • Wouldn't Promise be undefined in JS environments without promises?
  • Why are we throwing the error instead of calling callback within setTimeout?
  • Why are we using a separate catch instead of passing the error handler to then?
  • How does this polyfill fallback to using setTimeout when “the promise can't be created”?
  • When would the promise not be created?

I would have expected the polyfill to be implemented as follows.

if (typeof window.queueMicrotask !== "function") {
  window.queueMicrotask = callback =>
    typeof Promise === "function" && typeof Promise.resolve === "function"
      ? Promise.resolve().then(callback)
      : setTimeout(callback, 0);
}

What's the reason why it's not implemented so?

Edit: I was going through the mit history of the queue-microtask library and I found this mit.

@@ -1,9 +1,8 @@
-let resolvedPromise
+let promise

 module.exports = typeof queueMicrotask === 'function'
   ? queueMicrotask
-  : (typeof Promise === 'function' ? (resolvedPromise = Promise.resolve()) : false)
-    ? cb => resolvedPromise
-      .then(cb)
-      .catch(err => setTimeout(() => { throw err }, 0))
-    : cb => setTimeout(cb, 0)
+  // reuse resolved promise, and allocate it lazily
+  : cb => (promise || (promise = Promise.resolve()))
+    .then(cb)
+    .catch(err => setTimeout(() => { throw err }, 0))

So, it seems as though this library did indeed fallback to using cb => setTimeout(cb, 0). However, this was later removed. It might have been a mistake which went unnoticed. As for the MDN article, they might have just copied the snippet blindly from this library.

Consider the following polyfill for queueMicrotask.

if (typeof window.queueMicrotask !== "function") {
  window.queueMicrotask = function (callback) {
    Promise.resolve()
      .then(callback)
      .catch(e => setTimeout(() => { throw e; }));
  };
}

The description on MDN states.

It creates a microtask by using a promise that resolves immediately, falling back to using a timeout if the promise can't be created.

The queue-microtask library also uses the same polyfill. Here's what its documentation says.

  • Optimal performance in all modern environments.
    • Use queueMicrotask in modern environments (optimal)
    • Fallback to Promise.resolve().then(fn) in Node.js 10 and earlier, and old browsers (optimal)
    • Fallback to setTimeout in JS environments without Promise (slow)

This raises more questions than answers.

  • Wouldn't Promise be undefined in JS environments without promises?
  • Why are we throwing the error instead of calling callback within setTimeout?
  • Why are we using a separate catch instead of passing the error handler to then?
  • How does this polyfill fallback to using setTimeout when “the promise can't be created”?
  • When would the promise not be created?

I would have expected the polyfill to be implemented as follows.

if (typeof window.queueMicrotask !== "function") {
  window.queueMicrotask = callback =>
    typeof Promise === "function" && typeof Promise.resolve === "function"
      ? Promise.resolve().then(callback)
      : setTimeout(callback, 0);
}

What's the reason why it's not implemented so?

Edit: I was going through the mit history of the queue-microtask library and I found this mit.

@@ -1,9 +1,8 @@
-let resolvedPromise
+let promise

 module.exports = typeof queueMicrotask === 'function'
   ? queueMicrotask
-  : (typeof Promise === 'function' ? (resolvedPromise = Promise.resolve()) : false)
-    ? cb => resolvedPromise
-      .then(cb)
-      .catch(err => setTimeout(() => { throw err }, 0))
-    : cb => setTimeout(cb, 0)
+  // reuse resolved promise, and allocate it lazily
+  : cb => (promise || (promise = Promise.resolve()))
+    .then(cb)
+    .catch(err => setTimeout(() => { throw err }, 0))

So, it seems as though this library did indeed fallback to using cb => setTimeout(cb, 0). However, this was later removed. It might have been a mistake which went unnoticed. As for the MDN article, they might have just copied the snippet blindly from this library.

Share Improve this question edited May 3, 2020 at 5:21 Aadit M Shah asked May 3, 2020 at 4:51 Aadit M ShahAadit M Shah 74.3k31 gold badges175 silver badges307 bronze badges 6
  • why would you think it would do that? oh, wait, yes, I see, you're right – Jaromanda X Commented May 3, 2020 at 5:15
  • It seems both the MDN example and that library just assume Promise exists (a fair assumption given the desire to kill IE once and for all) – Jaromanda X Commented May 3, 2020 at 5:21
  • 1 In that case, it would never fallback to using setTimeout. That's false advertising. – Aadit M Shah Commented May 3, 2020 at 5:31
  • You are correct, it won't - you should add an issue to that library to mention that falsehood :p – Jaromanda X Commented May 3, 2020 at 5:33
  • 1 As for the MDN article, they might have just copied the snippet blindly from this library. the MDN "polyfill" pre-dates the creation of this library by one day - if anything this library copied the MDN polyfill (and fixed it) – Jaromanda X Commented May 3, 2020 at 5:55
 |  Show 1 more ment

1 Answer 1

Reset to default 10

You are entirely right in your main points, this polyfill won't work if there is no Promise in the environment, and I did edit the MDN article to now call it a "monkey-patch" as it is what it is and I removed the reference to "fallback" as there isn't.

To answer your questions:

  • Yes Promise would be undefined and thus the polyfill would just throw:

delete window.queueMicrotask;
delete window.Promise;

if (typeof window.queueMicrotask !== "function") {
  window.queueMicrotask = function (callback) {
    Promise.resolve()
      .then(callback)
      .catch(e => setTimeout(() => { throw e; }));
  };
}

queueMicrotask( () => console.log('hello') );
But this "shim" is apparently only aimed at "modern engines".

  • The MDN editor that did introduce that exception throwing here did so because the specs ask that queueMicroTask reports any exception that would be thrown during callback execution. The Promise chain would "swallow" this exception (it wouldn't get thrown globally), so to get out of this Promise chain, we have to call setTimeout from inside the .catch() handler.

  • Handling from the second parameter of then wouldn't handle Exceptions thrown from the callback execution, which is just what we want to do here.

  • It doesn't fallback to anything else than Promise, as we shown in the previous bullets it would just throw in case Promise is not defined, and the setTimeout is only used to throw the Exception out of the Promise chain.

  • The Promise would not be created by Promise.resolve() when that function would be something else than a correct Promise implementation. And if it's the case, there is like no chance it returns a catchable object either ;) But As you may have caught now, only the explanation text was pletely mislead+ing.


Now, a note about your monkey-patch which still might be improvable a little bit:

  • This editor was actually correct that the error should be reported, the catch + setTimeout should be there.

  • queueMicrotask should throw if callback is not Callable.

  • Nitpick, but the callback passed to .then() will be called with one argument undefined, queueMicrotask calls its callback without any argument.

  • Nitpick again, checking every time if Promise is available doesn't sound great, either Promise is defined from the beginning, either you'll use a polyfill you don't know how they managed the asynchronicity.

  • More importantly (?) you may want to add support for more environments.


The queue a microtask algorithm was already part of the Web standards before Promises make their way to browsers: MutationObserver queues microtasks too, and it was supported in IE11 (unlike Promises).

function queueMutationObserverMicrotask( callback ) {
  var observer = new MutationObserver( function() {
    callback();
    observer.disconnect();
  } );
  var target = document.createElement( 'div' );
  observer.observe( target, { attributes: true } );
  target.setAttribute( 'data-foo', '' );
}

Promise.resolve().then( () => console.log( 'Promise 1' ) );
queueMutationObserverMicrotask( () => console.log('from mutation') );
Promise.resolve().then( () => console.log( 'Promise 2' ) );

In node.js < 0.11, process.nextTick() was the closest to what microtasks are, so you may want to add it too (it's short enough).

if( typeof process === "object" && typeof process.nextTick === "function" ) {
  process.nextTick( callback );
}

So all in all, our improved polyfill would look like

(function() {
'use strict';

// lazy get globalThis, there might be better ways
const globalObj = typeof globalThis === "object" ? globalThis :
  typeof global === "object" ? global :
  typeof window === "object" ? window :
  typeof self === 'object' ? self :
  Function('return this')();

if (typeof queueMicrotask !== "function") {

  const checkIsCallable = (callback) => {
    if( typeof callback !== 'function' ) {
      throw new TypeError( "Failed to execute 'queueMicrotask': the callback provided as parameter 1 is not a function" );
    }  
  };

  if( typeof Promise === "function" && typeof Promise.resolve === "function" ) {
    globalObj.queueMicrotask = (callback) => {
      checkIsCallable( callback );
      Promise.resolve()
        .then( () => callback() ) // call with no arguments
        // if any error occurs during callback execution,
        // throw it back to globalObj (using setTimeout to get out of Promise chain)
        .catch( (err) => setTimeout( () => { throw err; } ) );
   };
  }
  else if( typeof MutationObserver === 'function' ) {
    globalObj.queueMicrotask = (callback) => {
      checkIsCallable( callback );
      const observer = new MutationObserver( function() {
        callback();
        observer.disconnect();
      } );
      const target = document.createElement( 'div' );
      observer.observe( target, { attributes: true } );
      target.setAttribute( 'data-foo', '');
    };
  }
  else if( typeof process === "object" && typeof process.nextTick === "function" ) {
    globalObj.queueMicrotask = (callback) => {
      checkIsCallable( callback );
      process.nextTick( callback );
    };
  }
  else {
    globalObj.queueMicrotask = (callback) => {
      checkIsCallable( callback );
      setTimeout( callback, 0 );
    }
  }
}
})();

queueMicrotask( () => console.log( 'microtask' ) );
console.log( 'sync' );

发布评论

评论列表(0)

  1. 暂无评论