以下 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)
这提出的问题多于答案。
- 在没有 promises 的 JS 环境中
Promise
不会 undefined
吗?
- 为什么我们抛出错误而不是在
setTimeout
中调用 callback
?
- 为什么我们使用单独的
catch
而不是将错误处理程序传递给 then
?
- 当“无法创建承诺”时,这个 polyfill 如何回退到使用
setTimeout
?
- 什么时候不创建承诺?
我原以为 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 exception 在 callback
执行期间抛出的 MDN 编辑器。 Promise 链会“swallow”这个异常(它不会被全局抛出),所以要离开这个 Promise 链,我们必须从内部调用 setTimeout
.catch()
处理程序。
从 then
的第二个参数处理不会处理从回调执行中抛出的异常,这正是我们想要在这里做的。
它不会退回到 Promise
之外的任何其他内容,正如我们在前面的项目符号中所示,它只会在未定义 Promise
的情况下抛出,并且 setTimeout
仅用于将 Exception 抛出 Promise 链。
当该函数不是正确的 Promise 实现时,Promise.resolve()
不会创建 Promise。如果是这样的话,它 returns 也不可能是一个可捕捉的对象 ;) 但是正如您现在可能已经捕捉到的那样,只有解释文本完全误导了。
现在,关于您的 monkey-patch 的说明,它仍然可以改进一点:
这位编辑实际上是正确的 error should be reported,catch
+ setTimeout
应该在那里。
如果 callback 不是 Callable.[=,queueMicrotask
应该抛出102=]
吹毛求疵,但是传递给 .then()
的回调将使用一个参数调用 undefined
,queueMicrotask
调用其回调时不带任何参数。
再次吹毛求疵,每次检查 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' );
考虑以下 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)
这提出的问题多于答案。
- 在没有 promises 的 JS 环境中
Promise
不会undefined
吗? - 为什么我们抛出错误而不是在
setTimeout
中调用callback
? - 为什么我们使用单独的
catch
而不是将错误处理程序传递给then
? - 当“无法创建承诺”时,这个 polyfill 如何回退到使用
setTimeout
? - 什么时候不创建承诺?
我原以为 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') );
did introduce that exception throwing here did so because the specs ask that
queueMicroTask
reports any exception 在callback
执行期间抛出的 MDN 编辑器。 Promise 链会“swallow”这个异常(它不会被全局抛出),所以要离开这个 Promise 链,我们必须从内部调用setTimeout
.catch()
处理程序。从
then
的第二个参数处理不会处理从回调执行中抛出的异常,这正是我们想要在这里做的。它不会退回到
Promise
之外的任何其他内容,正如我们在前面的项目符号中所示,它只会在未定义Promise
的情况下抛出,并且setTimeout
仅用于将 Exception 抛出 Promise 链。当该函数不是正确的 Promise 实现时,
Promise.resolve()
不会创建 Promise。如果是这样的话,它 returns 也不可能是一个可捕捉的对象 ;) 但是正如您现在可能已经捕捉到的那样,只有解释文本完全误导了。
现在,关于您的 monkey-patch 的说明,它仍然可以改进一点:
这位编辑实际上是正确的 error should be reported,
catch
+setTimeout
应该在那里。
如果 callback 不是 Callable.[=,queueMicrotask
应该抛出102=]吹毛求疵,但是传递给
.then()
的回调将使用一个参数调用undefined
,queueMicrotask
调用其回调时不带任何参数。再次吹毛求疵,每次检查 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' );