macro-task和micro-task 简介

先看下面的代码

<script>
        console.log(1);
        setTimeout(function(){
            console.log(2);
        },0);
        console.log(3);
    </script>

上面的执行结果是1,3,2
原因:上面的setTimeout可以理解为异步函数调用,因为javascript是单线程的,主线程拥有一个执行栈和一个事件循环
当代码开始执行的时候,主线程会依次执行代码(就是script里面的代码),当遇到异步函数的时候(setTimeout),会将该函数加入到任务队列里面,然后继续执行,当主线程空闲后,然后把异步函数出栈,直到所有的异步函数执行完毕即可。

在一个浏览器环境中,只能有一个事件循环,但是可以有多个任务队列,而每个任务都有一个任务源,相同任务源的任务,只能放到一个任务队列中

上面的micro-task和macro-task就是两种不同的任务队列

macro-task:script(script标签里面的整体代码) setTimeout,setInterval,setImmediate,I/O,UI rendering
micro-task:process.nextTick,Promise,MutationObserver

具体流程
首先:全部代码(script)算一个macrotask.。
第一步:浏览器先执行一个macrotask;执行的过程中,创造了新的macrotask(setTimeout之类的),然后接着执行,把promise加入到micro-task队列里面
第二步:浏览器执行microtask(例如promise),这里会将microtask里面所有任务都取出
第三步:重复,浏览器会再执行一个macrotask
总的来说:macrotask每次只取一个,而microtask会一次取完

下面再来另外一个例子:

<script>
        console.log(1);
        setTimeout(function(){
            console.log(2);
        },0)
        Promise.resolve().then(function(){
            console.log(3);
        }).then(function(){
            console.log(4);
        })
        console.log(5)
               
    </script>

上面的输出结果是 1,5,3,4,2

具体流程大概是下面这样:

当代码开始执行的时候,会先输出1,然后把setTimeout加入到一个macrotask 队列里面,接着把promise加入到microtask 队列里面,然后输出5
到这里我们代码执行完了一个macrotask(script里面的代码算第一个macrotask),接着要开始执行microtask,这次会把microtask里面所有的任务都执行完,这里就输出3和4,
当microtask执行完后,又会接着开始执行macrotask,就是setTimeout,到这里输出完2后,所有代码就都执行完毕

有个疑问 啊嘞嘞?是什么原因导致了原本应该在setTimeout回调后面的Promise的回调反而跑到前面去执行了呢?

为了搞清这个问题,我专门去翻阅了一下资料,首先找到了Promises/A+标准里面提到:

  • 一个事件循环有一个或者多个任务队列;
  • 每个事件循环都有一个microtask队列
  • macrotask队列就是我们常说的任务队列,microtask队列不是任务队列
  • 一个任务可以被放入到macrotask队列,也可以放入microtask队列
  • 当一个任务被放入microtask或者macrotask队列后,准备工作就已经结束,这时候可以开始执行任务了。

可见,setTimeout和Promises不是同一类的任务,处理方式应该会有区别,具体的处理方式有什么不同呢?我从这篇文章里找到了下面这段话:

通俗的解释一下,microtasks的作用是用来调度应在当前执行的脚本执行结束后立即执行的任务。 例如响应事件、或者异步操作,以避免付出额外的一个task的费用。
microtask会在两种情况下执行:

1.任务队列(macrotask = task queue)回调后执行,前提条件是当前没有其他执行中的代码。
2.每个task末尾执行。
另外在处理microtask期间,如果有新添加的microtasks,也会被添加到队列的末尾并执行。

也就是说执行顺序是:

开始 -> 取task queue第一个task执行 -> 取microtask全部任务依次执行 -> 取task queue下一个任务执行 -> 再次取出microtask全部任务执行 -> ... 这样循环往复

Promise一旦状态置为完成态,便为其回调(.then内的函数)安排一个microtask。

接下来我们看回我们上面的代码

 setTimeout(function(){
        console.log(1)
    },0);
    new Promise(function(resolve){
        console.log(2)
        for( var i=100000 ; i>0 ; i-- ){
            i==1 && resolve()
        }
        console.log(3)
    }).then(function(){
        console.log(4)
    });
    console.log(5);

按照上面的规则重新分析一遍:

1.当运行到setTimeout时,会把setTimeout的回调函数console.log(1)放到任务队列里去,然后继续向下执行。
2.接下来会遇到一个Promise。首先执行打印console.log(2),然后执行for循环,即时for循环要累加到10万,也是在执行栈里面,等待for循环执行完毕以后,将Promise的状态从fulfilled切换到resolve,随后把要执行的回调函数,也就是then里面的console.log(4)推到microtask里面去。接下来马上执行马上console.log(3)。
3.然后出Promise,还剩一个同步的console.log(5),直接打印。这样第一轮下来,已经依次打印了2,3,5。
4.现在第一轮任务队列已经执行完毕,没有正在执行的代码。符合上面讲的microtask执行条件,因此会将microtask中的任务优先执行,因此执行console.log(4)
5.最后还剩macrotask里的setTimeout放入的函数console.log(1)最后执行。

如此分析输出顺序是:

    2
    3
    5
    4
    1

看吧,这次分析对了呢ヾ(◍°∇°◍)ノ゙

总结和参考资料

microtask和macrotask看起来容易混淆,实际上还是很好区分的。macrotask就是我们常说的任务队列(task queue)。

JavaScript执行顺序可以简要总结如下:

开始 -> 取task queue第一个task执行 -> 取microtask全部任务依次执行 -> 取task queue下一个任务执行 -> 再次取出microtask全部任务执行 -> ...

循环往复,直至两个队列全部任务执行完毕。

Tasks, microtasks, queues and schedules
github-setImmediate.js
知乎-Promise的队列与setTimeout的队列有何关联?
阮一峰-JavaScript 运行机制详解:再谈Event Loop
百度

posted @ 2020-04-13 11:19  打个大大西瓜  阅读(457)  评论(0编辑  收藏  举报