资讯专栏INFORMATION COLUMN

浏览器环境下的microtaks和macrotasks

econi / 2326人阅读

摘要:的回调函数正是处于队列之中。将看做会导致性能问题,回调函数可能会因为渲染等相关产生不必要的延后。浏览器是怎么出错的和在两次点击操作之间运行完成了所有的,就比如的回调函数所展示的,但是似乎有不同的排序算法。

带有可视代码执行顺序的原文链接https://jakearchibald.com/201...,
此篇文字并非其完整翻译,加入了一部分自己的理解,比如将其中的task替换为macrotask或是删除了可视代码执行顺序的逐步解释。
运行顺序

参考以下JavaScript代码:

    console.log("script start");

    setTimeout(function() {
    console.log("setTimeout");
    }, 0);

    Promise.resolve().then(function() {
    console.log("promise1");
    }).then(function() {
    console.log("promise2");
    });

    console.log("script end");
    /*
     * script start
     * script end
     * promise1
     * promise2
     * setTimeout
     */

但是,在 Microsoft Edge, Firefox 40, iOS Safari 和 桌面版 Safari 8.0.8 中,setTimeout会优先于promise1promise2。而令人奇怪的是,在 Firefox 39 和 Safari 8.0.7 中又是一致的。

为什么会这样

Macrotask

想要理解这部分内容,你需要知道事件循环和microtasks。如果你是第一次接触相关内容,可能会需要一些精力,别紧张,大家都会这样,深呼吸…

在浏览器中,每一个thread(可以理解为每一个页签)都有自己的事件循环,因此,它们可以相互独立执行自身的Macrotask,然而,同源的窗口会分享同一个事件循环来保证相互可以进行同步通讯行为。事件循环会持续运行下去,用于执行当前存在的所有任务列表。每一个事件循环存在多个不同的任务队列用以保证执行顺序,而浏览器会依照任务类别来从任务序列中选取一个任务来进行执行。这使得浏览器可以优先选择执行更为重要的任务,比如用户输入操作。

Macrotask是已经被排序完成的,因此浏览器可以通过内部的机制来直接将其放置于javascript/DOM程序域中并确保每一个程序步骤的顺序执行。而在两个任务执行间隔之中,浏览器 可能 会执行更新操作。比如处理获取用户点击的回调函数,分析HTML,又或者是setTimeout

setTimeout等待一个指定的时间延迟然后加入一个新的任务来执行对应的回调函数。这就是为什么setTimeout会延迟于script end,因为script end是第一个任务的程序内容,而setTimeout是来之后续的另一个任务。

Microtasks

Microtasks通常用于排列那些应当在当前任务执行完毕后立即执行的任务,比如对某些事件作出反应,或是一些不会影响新任务的异步操作。这个Microtasks序列是在没有其他JavaScript任务正在执行,同时在其他Macrotask执行完毕之后。任何新添加的Microtasks会被排列到Microtasks的队尾并进行处理。promise的回调函数正是处于Microtasks队列之中。

当一个promise结束掉以后,或者它在之前已经处理完毕,那么会添加一个回馈结果的回调函数至Microtasks的队尾。这确保了promise的回调函数永远是异步执行的,即使promise已经在当前的时间片执行完毕。因此在调用.then(yey,nay)时并不会直接将一个Macrotask添加至队尾。这就是为什么promise1promise2会晚于script end,当前运行的Macrotask一定会在Macrotask处理前执行完毕。promise1promise2早于setTimeout输出,则是因为microtasks永远在下一个Macrotask启动前结束。

为什么有些浏览器表现不一致

有些浏览器的输出顺序为:script start, script end, setTimeout, promise1, promise2。它们在执行setTimeout后才运行primise的回调函数。这就好像是它们更倾向于将promise的回调函数看做Macrotask的一类。

这其实是可以理解的,promise是来自于ECMAScript而非HTML。ECMAScript拥有一个类似于Macrotask的"jobs"的概念,但这种关系并不能很清晰的区分开vague mailing list discussions。无论如何,更为普遍的观点是,promise是属于microtask,并且有一些很好的理由。

将promise看做Macrotask会导致性能问题,回调函数可能会因为渲染等相关Macrotask产生不必要的延后。同时也会导致影响其他的Macrotask,并且可能打断和其他api的交互,并导致其延后。

这里有个将promise当做microtasks处理的类似说明,an Edge ticket。WebKit内核的做法显然是正确的,因此我推断Safari最终也会选择修复这个问题,同时Firefox43似乎也已经修复了这个问题。

如何判断是Macrotask还是Microtask

直接进行测试是一种办法。在浏览器中直接查看关于promisesetTimeout的输出,尽管你依赖的实现是正确的。

就像之前所提到的,在ECMAScript中,它们称microtasks为“jobs”。在step 8.a of PerformPromiseThen中,EnqueueJob被称为添加一个microtask。

现在,让我们看一个更复杂的例子。

加入MutationObserver

首先让我们写一段html代码:

接下来是一段JS:

// Let"s get hold of those elements
var outer = document.querySelector(".outer");
var inner = document.querySelector(".inner");

// Let"s listen for attribute changes on the
// outer element
new MutationObserver(function() {
  console.log("mutate");
}).observe(outer, {
  attributes: true
});

// Here"s a click listener…
function onClick() {
  console.log("click");

  setTimeout(function() {
    console.log("timeout");
  }, 0);

  Promise.resolve().then(function() {
    console.log("promise");
  });

  outer.setAttribute("data-random", Math.random());
}

// …which we"ll attach to both elements
inner.addEventListener("click", onClick);
outer.addEventListener("click", onClick);

    /*
     *click
     *promise
     *mutate
     *click
     *promise
     *mutate
     *timeout
     *timeout
     */

在不同浏览器中的表现:
Chrome:
click
promise
mutate
click
promise
mutate
timeout
timeout

FireFox:
click
mutate
click
mutate
timeout
promise
promise
timeout

Safari:
click
mutate
click
mutate
promise
promise
timeout
timeout

Edge:
click
click
mutate
timeout
promise
timeout
promise

哪个是正确的

抛出‘click’事件的是一个macrotask,Mutation observer 和 promise 的回调函数被当做microtask进行排列。setTimeout的回调会被当做一个 macrotask。

因此Chrome的运行结果才是正确的。这里有点奇特的地方反而是microtask在回调函数之后执行(直到没有其他的代码在执行),我认为这里是限制了marcotask的完成。这条用于限制回调函数的规则来源自HTML:

If the stack of script settings objects is now empty, perform a microtask checkpoint
— HTML: Cleaning up after a callback step 3

同时一个microtask checkpoint遍历了整个microtask队列,除非我们已经在执行microtask队列。类似的,ECMAScript 描述了jobs:

Execution of a Job can be initiated only when there is no running execution context and the execution context stack is empty…(Job可以在没有可执行环境和可执行环境的堆为空的情况下被初始化)
— ECMAScript: Jobs and Job Queues

尽管这里的“can be”在HTML环境中变成了“must be”。

浏览器是怎么出错的?

FirefoxSafari在两次点击操作之间运行完成了所有的microtasks,就比如mutation的回调函数所展示的,但是promise似乎有不同的排序算法。这是可以理解的,因为jobs和microtasks之间的联系是相对模糊的,但我依然可以确定他们会在两次点击回调操作之间运行完成。Firefox ticket.Safari ticket.

对于Edge我们已经可以确定它对于promise的队列类别是不正确的,但它依然在两次点击回调操作之间运行完成了所有的microtasks,相反的是它是在调用完成了所有的监听回调后,两次点击操作仅仅触发了一次mutate。Bug ticket

试试更复杂的

现在我们仅仅在代码最后加入一行新的代码来取代点击操作:

// Let"s get hold of those elements
var outer = document.querySelector(".outer");
var inner = document.querySelector(".inner");

// Let"s listen for attribute changes on the
// outer element
new MutationObserver(function() {
  console.log("mutate");
}).observe(outer, {
  attributes: true
});

// Here"s a click listener…
function onClick() {
  console.log("click");

  setTimeout(function() {
    console.log("timeout");
  }, 0);

  Promise.resolve().then(function() {
    console.log("promise");
  });

  outer.setAttribute("data-random", Math.random());
}

// …which we"ll attach to both elements
inner.addEventListener("click", onClick);
outer.addEventListener("click", onClick);


inner.click();

这将会和上一个例子一样抛出点击事件,但我们使用代码来取代真实的点击交互。

试一试

Chrome:
click
click
promise
mutate
promise
timeout
timeout

FireFox:
click
click
mutate
timeout
promise
promise
timeout

Safari:
click
click
mutate
promise
promise
timeout
timeout

Edge:
click
click
mutate
timeout
promise
timeout
promise

为什么会这样

在所有的监听回调触发完成后…

If the stack of script settings objects is now empty, perform a microtask checkpoint
— HTML: Cleaning up after a callback step 3

在上一个的例子中,microtasks会在两个点击回调之间运行,但.click()使得两次事件顺序同步执行,因此在两次点击回调之间依然存在js代码在运行。而上面的规则确保了microtasks不会打断正在执行的代码片段。这意味着我们不能在两次点击监听之间执行microtasks队列,它们将会在监听回调执行完成后开始运行。

总结

Macrotask会顺序执行,浏览器可能会在其执行间隔中进行渲染操作

Microtask会顺序执行:

在所有的回调完成之后,且不存在其他的js代码正在执行

在每一个macrotask完成之后

希望你现在已经清楚了事件循环的相关内容,或者至少可以去偷个懒休息一下。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/51616.html

相关文章

  • 览器环境下的microtaksmacrotasks

    摘要:的回调函数正是处于队列之中。将看做会导致性能问题,回调函数可能会因为渲染等相关产生不必要的延后。浏览器是怎么出错的和在两次点击操作之间运行完成了所有的,就比如的回调函数所展示的,但是似乎有不同的排序算法。 带有可视代码执行顺序的原文链接https://jakearchibald.com/201...,此篇文字并非其完整翻译,加入了一部分自己的理解,比如将其中的task替换为macrot...

    FreeZinG 评论0 收藏0
  • Vue nextTixk与任务

    摘要:以上函数只有是将回调放进队列中,所以是最优方案,只有在不存在的情况下才会走其他方法。也是将回调函数放进中,优点是不需要做超时检测,目前只有浏览器实现。 js的macrotask和microtask js每次事件循环只从macrotask中读取一个并任务执行,同一个事件循环会把microtask中的任务执行完毕并且先于macrotask 为什么要将数据更新的处理函数放在microtask...

    leonardofed 评论0 收藏0
  • 浅谈不同环境下的JavaScript执行机制 + 示例详解

    摘要:如果没有其他异步任务要处理比如到期的定时器,会一直停留在这个阶段,等待请求返回结果。执行的执行事件关闭请求的,例如事件循环的每一次循环都需要依次经过上述的阶段。因此,才会早于执行。 showImg(https://segmentfault.com/img/bVbnY76); 概念 同步任务(Synchronous) 在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务 ...

    wanghui 评论0 收藏0
  • 理解javascript中的事件循环(Event Loop)

    摘要:主线程会暂时存储等异步操作,直接向下执行,当某个异步事件触发时,再通知主线程执行相应的回调函数,通过这种机制,避免了单线程中异步操作耗时对后续任务的影响。 背景 在研究js的异步的实现方式的时候,发现了JavaScript 中的 macrotask 和 microtask 的概念。在查阅了一番资料之后,对其中的执行机制有所了解,下面整理出来,希望可以帮助更多人。 先了解一下js的任务执...

    mykurisu 评论0 收藏0
  • 览器知识

    摘要:浏览器的渲染进程是多线程的。异步请求线程在在连接后是通过浏览器新开一个线程请求将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。 [TOC] 浏览器进程线程 区分线程和进程 **- 什么是进程** 狭义定义:进程是正在运行的程序的实例(an instance of a computer program that is being exe...

    Pluser 评论0 收藏0

发表评论

0条评论

econi

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<