以下 queueMicrotask polyfill 如何回退到使用 setTimeout?

How does the following queueMicrotask polyfill fallback to using setTimeout?

考虑以下 polyfill for queueMicrotask

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

MDN 上的描述说明。

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

queue-microtask 库也使用相同的 polyfill。这是它的文档所说的。

  • 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)

这提出的问题多于答案。

我原以为 polyfill 会按如下方式实现。

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

为什么没有实施呢?

编辑: 我正在查看队列微任务库的提交历史,我发现 this commit.

@@ -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))

所以,这个库似乎确实退回到了使用 cb => setTimeout(cb, 0)。不过,这后来被删除了。这可能是一个没有引起注意的错误。至于 MDN 文章,他们可能只是盲目地从这个库中复制了片段。

你的要点是完全正确的,如果环境中没有 Promise,这个 polyfill 将无法工作,我确实编辑了 MDN 文章,现在称它为 "monkey-patch",因为它是什么是的,我删除了对 "fallback" 的引用,因为没有。

回答您的问题:

  • 是的 Promise 将是未定义的,因此 polyfill 将抛出:

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') );
但是这个 "shim" 显然是 only aimed at "modern engines".

  • did introduce that exception throwing here did so because the specs ask that queueMicroTask reports any exceptioncallback 执行期间抛出的 MDN 编辑器。 Promise 链会“swallow”这个异常(它不会被全局抛出),所以要离开这个 Promise 链,我们必须从内部调用 setTimeout .catch() 处理程序。

  • then 的第二个参数处理不会处理从回调执行中抛出的异常,这正是我们想要在这里做的。

  • 它不会退回到 Promise 之外的任何其他内容,正如我们在前面的项目符号中所示,它只会在未定义 Promise 的情况下抛出,并且 setTimeout 仅用于将 Exception 抛出 Promise 链。

  • 当该函数不是正确的 Promise 实现时,Promise.resolve() 不会创建 Promise。如果是这样的话,它 returns 也不可能是一个可捕捉的对象 ;) 但是正如您现在可能已经捕捉到的那样,只有解释文本完全误导了。


现在,关于您的 monkey-patch 的说明,它仍然可以改进一点:

  • 这位编辑实际上是正确的 error should be reportedcatch + setTimeout 应该在那里。

  • 如果 callback 不是 Callable.

    [=,
  • queueMicrotask 应该抛出102=]

  • 吹毛求疵,但是传递给 .then() 的回调将使用一个参数调用 undefinedqueueMicrotask 调用其回调时不带任何参数。

  • 再次吹毛求疵,每次检查 Promise 是否可用听起来不太好,要么从一开始就定义了 Promise,要么你会使用 polyfill 你不知道他们是如何管理的异步性。

  • 更重要的是 (?) 您可能希望添加对更多环境的支持。


queue a microtask algorithm was already part of the Web standards before Promises make their way to browsers: MutationObserver queues microtasks too,IE11 支持它(与 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' ) );

在 node.js < 0.11 中,process.nextTick() 最接近微任务,因此您可能也想添加它(足够短)。

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

所以总而言之,我们改进的 polyfill 看起来像

(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' );