事件轮询中的task与microtask

event loop

  网上看到的一篇文章,关于介绍task和Tasks, microtasks, queues and schedules,尝试简单翻译一下写进来吧!

  原文地址:https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/

 

  当我跟我同事Matt Gaunt讲,我要写一篇关于microtask和浏览器事件轮询的文章的时候,他说:“你尽管写,反正我不看。”好吧,不看就算了,但我还是要写,总有人会看的。

  事实上, Philip Roberts已经对这方面的知识做了一个很完整的介绍,尽管没有包含microtasks,但是其他的基本上都有。好了,我要开始我的表演了!

  考虑下面的代码:

复制代码
    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 and desktop Safari 8.0.8中,setTimeout可能会在promise1和promise2之前打印-看起来就像是在竞争。看起来很奇怪,一般都是正确打印的。

 

这是为啥呢?

  想要理解这个,必须先了解事件轮询中的tasks与microtasks。这里面包含不少知识,第一次接触这个可能会让你脑阔疼,请深呼吸:

  每一个‘线程’都有它独立的事件轮询,所以每个页面都可以各自工作,执行它们自己的代码。所有一个来源的窗口都共享同一个事件轮询,彼此之间同步交流信息。事件轮询不断的运转,执行所有的任务队列。一个事件轮询中的任务可能来源于多个地方,需要保证所有任务按正确的顺序执行并不简单,但是浏览器会帮忙选择如何执行这些任务。这样一来,浏览器可以对一些影响性能的操作(如用户输入)做特殊处理。跟上!

  Tasks已经被提前排好序,保证了浏览器可以持续从内部取出它们并弄到JS/DOM中执行。在两个任务的执行空隙,浏览器可能会重新渲染视图。在解析HTML页面的时候,鼠标点击事件与对应回调函数会产生一个新的task,同时会产生事件序列的还有上面的例子:setTimeout。

  setTimeout会延迟指定的时间,然后将回调函数加入任务序列中。这就是为什么setTimeout会在script end后面打印。script end的打印属于第一个任务序列的一部分,而setTimeout则在下一个任务序列中被打印。OK,这里基本上没问题了,希望下一个环节你还能坚持住……

  Microtasks通常在JS当前主任务执行完后直接执行,比如说对一些特殊事件作出响应,或者在不影响主线程情况下异步执行某些事件。一旦没有其他JS代码在执行中,microtask队列会立即执行,执行过程中如果有microtask插入,也同时会被执行。microtas包括mutation observer回调,与上面例子中的promise回调。

  一旦一个promise被决议,在决议后就会形成一个microtask来响应回调函数。这个可以保证promise的即使被决议,回调函数也会被异步执行。因此,调用then(rel,rej)方法后会立即生成一个microtask队列。这也就是为什么promise1和promise2在script end后面打印,microtask必须在当前JS代码运转完后才会被操作。promise1和promise2在setTimeout之前打印,也就是microtask永远在下一个task之前执行。

  这样上面的例子就很清晰了:

复制代码
    //执行JS主代码
    console.log('script start');
    //等待下一轮task
    setTimeout(function() {
        console.log('setTimeout');
    }, 0);
    //then方法产生microtask
    Promise.resolve().then(function() {
        console.log('promise1');
        //又插入一个microtask 立即执行
        //执行完后进行下一轮task
    }).then(function() {
        console.log('promise2');
    });
    //JS主代码 第一轮task执行完会执行microtask
    console.log('script end');
复制代码

  如同注释所说的那样,一步一步得到了最后的结果。

 

为什么不同浏览器会出现差异?

  有些浏览器会打印script start,start end,setTimeout,promise1,promise2。promise的回调函数在setTimeout之后执行,看起来似乎将promise当成下一轮task而不是microtask。

  某种程度上可以理解这件事,promise来自于ECMA标准而不是HTML。ECMA标准中有'jobs'的概念,跟microtasks很相似,然而,仅仅通过一些类似邮件的讨论,这两者的区别并不是那么清晰。但是一般来说,都公认promise应该是microtask的一部分,而且确实比较好。

  promise一般用来解决性能问题,有些回调函数可能会因为渲染之类的事件导致延迟执行。(后面的没太看懂)

 

如何判定这是一个task还是一个microtask

  测试是一种方式。看看setTimeout和promise的打印顺序,当然保证结果是正确。

  比较稳妥的方式是看说明文档。(举的例子会跳转到一个新页面)

  稍微提一下,在ECMA标准中,microtask被叫做‘jobs’。在step 8.a of PerformPromiseThen中,排入队列被称为生成一个microtask序列。

  现在来看一个更加复杂的案例:

Lv1 BOSS

  写这部分之前,我先给点易错的案例。

  看看下面这一段html:

    <div class="outer">
        <div class="inner"></div>
    </div>

  JS代码如下:

复制代码
    var outer = document.querySelector('.outer');
    var inner = document.querySelector('.inner');
    new MutationObserver(function() {
        console.log('mutate');
    }).observe(outer, {
        attributes: true
    });
    function onClick() {
        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', onClick);
复制代码

  如果点击div.inner,会打印出什么呢?

    在查看答案之前自己分析一下。(提示:有些东西会不止打印一次)

  答案不一样?或许你是对的,因为不同浏览器打印的不一样。

  Chrome:click,promise,mutate,click,promise,mutate,timeoue,timeoue

  Firefox:click,mutate,click,mutate,timeoue,promise,promise,timeoue

  Safari:click,mutate,click,mutate,promise,promise,timeout,timeout

  IE:click,click,mutate,timeout,promise,timeout,promise

 

哪一个是对的?

  触发的click事件是一个task。Mutation observer和promise的回调函数是microtask。setTimeout是另外一个task。所以顺序这样是这样的;

复制代码
    new MutationObserver(function() {
        //紧跟在promise后面的microtask
        console.log('mutate');
    }).observe(outer, {
        attributes: true
    });
    function onClick() {
        //click第一个task
        console.log('click');
        //第二个task
        setTimeout(function() {
            console.log('timeout');
        }, 0);
        //promise产生一个microtask
        Promise.resolve().then(function() {
            console.log('promise');
        });
        //这句代码也会产生一个microtask
        outer.setAttribute('data-random', Math.random());
    }
复制代码

  过程大概是这样的:点击div.inner,click(第一个task)->timeout(第二个task)->promise(microtask)->mutate(microtask)。

  按照之前所描述的顺序:task->microtask->task,可以得到click,promise,mutate,timeout。但是由于冒泡的关系,外层div也会触发一遍上面的流程,所以最终结果是click,promise,mutate,click,promise,mutae,timeout,timeout。

  因此,Chrome是正确的。有一个地方对我来说很新鲜,microtask的回调会在没有其余运行中JS代码后执行,我理解为task的尾部。下面是HTML文档中对回调的说明:

  如果栈中JS环境对象为空,会执行microtask队列的检查。  

                          —HTML:Cleaning up after a callback

  microtask的检查包含:遍历microtask队列直到全部被执行。

  ECMA标准把这个称为jobs:

  只有当前环境没有任何东西在执行并且执行环境栈为空,job才能开始被执行。

                          —ECMAScript:Jobs and Job Queues

  在HTML环境中,'能'变成了'必须'。

 

为什么浏览器会出错?

  Firefox和Safari可以正确的区别microtask与click事件,比如mutation的回调函数,但是promise的处理不太一样。这个顺序会出现问题也是情有可原的,因为关于job和microtask之间区别非常模糊,我认为这两个在事件回调之间执行比较合理。 Firefox ticketSafari ticket(这里是两个相关bug讨论链接)

  至于Edge,它对promise的处理错的一塌糊涂,同时也未在两个监听事件之间执行microtask队列,等监听事件都完事了才调用microtask,并且两个click事件只打印了一次mutate。Bug ticket

 

Lv1 BOSS愤怒的哥哥 

  代码跟上面的一样,但是执行的代码变成了:

    inner.click();

  这个也会触发同样的事件,但是方式不是通过点击,而是直接用JS代码执行。

  答案如下:

  Chrome:click,click,promise,mutate,promise,timeout,timeout

  Firefox:click,click,mutate,timeout,promise,promise,timeout

  Safari:click,click,muate,promise,promise,timeout,timeout

  IE:click,click,mutate,timeout,promise,timeout,promise

  Chrome每次都会出现不同的结果,我专门弄了一个表来记录我测试出来的错误。如果你在Chrome中得到不一样的结果,在评论中告诉我版本号。

 

为啥不一样?

  来梳理一下流程。

  首先这里有一个不一样的地方,即之前提到的:这里是执行JS代码触发函数,不是事件触发。所以这里的顺序是task(执行JS代码)->task(onClick函数)->打印click->timeout(第二个task)->promise(microtask)->mutate(microtask)->打印click(冒泡)->timeout(第三个task)->promise(microtask),mutate只会触发一次(不太懂原理),主要区别在于在冒泡的时候,JS代码仍在执行,所以说microtask不会执行,必须等到第二个click打印才会触发。最后正确的结果是click,click,promise,mutate,promise,timeout,timeout,看起来Chrome又对了。

  microtask在两个事件监听触发后被调用。

  之前,microtask在监听回调之间执行,但是通过JS代码的函数调用,导致事件同步执行了,第一个回调结束后,JS主线程依旧在栈中。上述规则保证了microtask不打断JS主线程执行。这意味着这种情况下,microtask不能在监听回调之间执行,而需要在之后。

 

这没问题吗?

  可能你会在一些地方还存在疑惑。我曾经在试图创建a simple wrapper library for IndexedDB that uses promises遇到过这个问题,它比IDBRequest对象还要奇怪。这同时也让IDB变得好玩起来。almost makes IDB fun to use.

  当IDB成功执行一个事件,相关的事务对象变得没那么活跃了(transaction object becomes inactive after dispatching没看懂)。如果在事件执行期间创建了一个promise,该回调会在step4(?)之前执行,此时相关事件仍然在执行,这个现象只会在Chrome中出现,对渲染库有一点没用。

  在Firefox中你可以变通解决这个问题,因为promise的polyfill是用mutation observers实现的,正确的实现了microtask。Safari对这两个microtask一直处于纠结状态。不幸的是,IE/Edge中也会有问题,mutation不会在回调后执行。

  希望能从这些问题中找到一些共同点。

 

  总结一下:

  ·  task按顺序执行,浏览器可能在周期间隙里渲染视图

  ·  microtask也是按顺序执行,遵循下列规则:

    1.JS主线程没有程序执行

    2.主程序的尾部


猜你喜欢

转载自blog.csdn.net/zuggs_/article/details/80648542