浏览器环境下的microtaks和macrotasks
时间:2023-04-02 15:12:00
HTML
有原文链接,有可视化代码执行顺序https://jakearchibald.com/201。已添加,例如将任务替换为宏任务或删除可视化代码执行顺序的分步说明。运行顺序参考以下JavaScript代码:console.log('scriptstart');setTimeout(function(){console.log('setTimeout');},0);Promise.resolve().then(function(){console.log('promise1');}).then(function(){console.log('promise2');});console.log('脚本结束');/**scriptstart*scriptend*promise1*promise2*setTimeout*/但是,在MicrosoftEdge、Firefox40、iOSSafari和桌面Safari8.0.8中,setTimeout优先于promise1和promise2。奇怪的是,它在Firefox39和Safari8.0.7中再次保持一致。为何如此Macrotask要理解这部分内容,您需要了解事件循环和微任务。如果你是第一次接触相关内容,你可能需要一些精力,不要紧张,每个人都会这样做,深吸一口气......在浏览器中,每个线程(可以理解为每个选项卡)有自己的事件循环,因此,它们可以相互独立地执行自己的宏任务,但是,同源的窗口会共享同一个事件循环,以确保它们可以相互同步通信。事件循环继续运行,执行所有当前存在的任务列表。每个事件循环都有多个不同的任务队列来保证执行顺序,浏览器会根据任务类型从任务序列中选择一个任务执行。这允许浏览器优先执行更重要的任务,例如用户输入。宏任务已经排序,浏览器可以通过内部机制直接将它们放在javascript/DOM程序域中,保证每个程序步骤的顺序执行。在两次任务执行之间的间隔中,浏览器可能会执行一次更新操作。比如处理获取用户点击的回调函数,分析HTML,或者setTimeout。setTimeout等待一个指定的时间延迟,然后添加一个新的任务来执行相应的回调函数。这就是为什么setTimeout会延迟到scriptend之后,因为scriptend是第一个task的程序内容,而setTimeout是紧随其后的另一个task。MicrotasksMicrotasks通常用于排队当前任务完成后应该立即执行的任务,比如响应某些事件,或者一些不会影响新任务的异步操作。这个Microtasks序列是在没有其他JavaScript任务正在执行并且其他Macrotasks已完成执行之后。任何新添加的Microtasks都将排到Microtasks队列的末尾并进行处理。promise的回调函数在Microtasks队列中。当一个promise被解析时,或者它之前已经被解析,一个返回结果的回调函数被添加到Microtasks队列的末尾。这确保了promise的回调函数始终异步执行,即使promise已在当前时间片中解决。因此,在调用.then(yey,nay)时,并不会直接将一个Macrotask加到队尾。这就是为什么promise1和promise2会晚于scriptend,并且必须先执行当前正在运行的Macrotask,然后再处理Macrotask。Promise1和promise2输出早于setTimeout,因为微任务总是在下一个Macrotask开始之前结束。为什么有些浏览器的行为不一致?某些浏览器按以下顺序输出:脚本开始、脚本结束、setTimeout、promise1、promise2。它们在执行setTimeout后运行promise的回调函数。似乎他们更倾向于把promise的回调函数当做一种Macrotask。这其实可以理解,promise来自于ECMAScript而不是HTML。ECMAScript有一个类似于Macrotask的“工作”的概念,但这种关系并没有明确区分模糊的邮件列表讨论。不管怎样,更普遍的观点是promise是微任务,并且有一些很好的理由。将promises当作Macrotasks会导致性能问题,而回调函数可能会因为渲染和其他相关的Macrotasks而造成不必要的延迟。同时也会影响到其他的Macrotasks,可能会打断与其他API的交互,导致延迟。这是关于将承诺作为微任务处理的类似说明,即Edge票证。WebKit内核的做法显然是正确的,所以我推断Safari最终会选择修复这个问题,而Firefox43似乎已经修复了这个问题。如何判断是Macrotask还是Microtask直接测试是一种方式。直接在浏览器中查看有关promises和setTimeout的输出,即使您依赖实现是正确的。如前所述,在ECMAScript中,他们将微任务称为“作业”。在PerformPromiseThen的第8.a步,调用了EnqueueJob添加一个microtask。现在,让我们看一个更复杂的例子。加入MutationObserver首先让我们写一段html代码:
接下来是一段JS://让我们掌握那些elementsvarouter=document.querySelector('.outer');varinner=document.querySelector('.inner');//让我们监听//外部元素的属性变化newMutationObserver(function(){console.log('mutate');}).observe(outer,{attributes:true});//这是一个点击监听器...functiononClick(){console.log('click');setTimeout(function(){console.log('timeout');},0);Promise.resolve().then(function(){console.log('promise');});outer.setAttribute('data-random',Math.random());}//...我们将附加到两个元素inner.addEventListener('click',onClick);outer.addEventListener('click',点击);/**click*promise*mutate*click*promise*mutate*timeout*timeout*/在不同浏览器中的表现:Chrome:clickpromisemutateclickpromisemutatetimeouttimeoutFireFox:clickmutateclickmutatetimeoutpromisepromisetimeoutSafari:clickmutateclickmutatepromisepromisetimeouttimeoutEdge:clickclickmutatetimeoutpromisetimeoutpromise哪个是正确的将'click'事件作为宏任务抛出,Mutationobserver和promise的回调函数被安排为微任务,setTimeout的回调被视为宏任务。因此,Chrome的运行结果是正确的。这里有点奇怪的是,微任务是在回调函数之后执行的(直到没有其他代码被执行),我认为这限制了marcotask的完成。这条限制回调函数的规则来自于HTML:如果脚本设置对象的栈现在为空,执行一个microtaskcheckpoint—HTML:Cleaningupafteracallbackstep3同时,一个microtaskcheckpoint遍历整个microtask队列,除非我们已经在执行微任务队列。类似地,ECMAScript描述了作业:ExecutionofaJobcanbeinitiatedonlywhenthereisnorunningexecutioncontextandtheexecutioncontextstackisempty...isinitialized)——ECMAScript:JobsandJobQueues虽然这里的“可以”变成了“必须”是”在HTML环境中。浏览器是怎么出错的?Firefox和Safari在点击之间运行所有微任务,如变异回调所示,但承诺似乎有不同的排序算法。这是可以理解的,因为jobs和microtasks之间的关系比较模糊,但我仍然可以肯定它们会在两次点击回调操作之间运行到完成。Firefox门票。Safari门票。对于Edge我们已经可以确认它的promise队列类型是错误的,但是它仍然运行了两次点击回调动作之间的所有微任务,而是调用了完成的所有监听器回调之后,两次点击只触发了一次mutate。Bugticket尝试更复杂的东西现在我们只需在代码末尾添加一行新代码来替换点击://让我们抓住那些元素varouter=document.querySelector('.outer');varinner=document.querySelector('.inner');//让我们监听//外部元素的属性变化newMutationObserver(function(){console.log('mutate');}).observe(outer,{attributes:true});//这是一个点击监听器...functiononClick(){console.log('click');setTimeout(function(){console.log('timeout');},0);Promise.resolve().then(function(){console.log('promise');});outer.setAttribute('data-random',Math.random());}//...我们将附加到两个元素inner.addEventListener('click',onClick);outer.addEventListener('click',点击);inner.click();这将像前面的示例一样抛出点击事件,但我们使用代码而不是实际的点击交互。试一试Chrome:clickclickpromisemutatepromisetimeouttimeoutFireFox:clickclickmutatetimeoutpromisepromisetimeoutSafari:clickclickmutatepromisepromisetimeouttimeoutEdge:clickclickmutatetimeoutpromisetimeoutpromise为什么会这样在所有的监听回调触发完成后…Ifthestackofscriptsettingsobjectsisnowempty,performamicrotaskcheckpoint—HTML:Cleaningupafteracallbackstep3在在前面的例子中,microtasks会运行在两次点击回调之间,但是.click()使得两个事件序列同步执行,所以两次点击回调之间仍然有js代码在运行。上述规则确保微任务不会中断正在执行的代码片段。这意味着我们不能在两个点击监听器之间执行微任务队列,它们将在监听器回调执行完成后开始运行。总结Macrotask会顺序执行,浏览器可能会在它的执行间隔内进行渲染操作。Microtask会顺序执行:在所有回调完成后,没有其他js代码在执行。每个宏任务完成后,我希望你现在清楚我已经了解了事件循环,或者至少我可以休息一下。