• 事件轮询中的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.主程序的尾部

      希望现在你能明白关于事件轮询的相关概念,至少会分析执行顺序。

      有人在看么。。。(作者原话)

      鸣谢A,B,C……

      

  • 相关阅读:
    Node.js NPM 包(Package)
    Node.js NPM 作用
    Node.js NPM 介绍
    Node.js NPM 教程
    Node.js NPM 教程
    Node.js 发送Email
    Node.js 上传文件
    Node.js 事件
    Node.js NPM
    Node.js 文件系统模块
  • 原文地址:https://www.cnblogs.com/QH-Jimmy/p/6493389.html
Copyright © 2020-2023  润新知