宏任务和微任务

494 阅读4分钟

最近看事件循环的面试题的时候发现自己对宏任务和微任务还没完全掌握,之前的记住的就是先执行宏任务再执行微任务,但是实际上好像并不是这样,然后就到处去找资料,看了一些博客和文档之后有了自己的一些见解,如有错误希望大家帮忙指出。

同步任务和异步任务

首先,js是单线程的脚本语言,既然是单线程的脚本那么就会产生一系列的问题,比如我发出一个请求,这时难道需要一直等到请求返回结果了再接着进行下面的脚本吗?显然是不合理的,那怎么办呢,这个时候需要一个规则来针对这些需求进行一个统一的规定。

js将任务分为了同步任务异步任务,同步任务是指在主线程上排队执行的任务,异任务指的是,不进入主线程、而进入“任务队列”的任务,只有在主线程上的任务都执行完之后才会执行“任务队列”里面的任务。

宏任务和微任务

将任务分为同步和异步并不能满足一些“vip”(比如上海封闭的时候做核酸,工作日可以让学生插队)的需求,这时就需要一个插队的机制,于是就有了宏任务和微任务。

我们先来了解一下那些方法或者事件可以开启宏任务或者微任务

  • 宏任务:script、setTimeout、setInterval、postMessage、MessageChannel、setImmediate(Node.js 环境)
  • 微任务:Promise.then、Object.observe(已废弃)、MutationObserver、process.nextTick(Node.js 环境)

事件循环

当我们知道了哪些可以开启宏任务和微任务之后,我们就可以开始了解js的事件循环机制了。

事件循环机制:先执行同步代码,遇到宏任务则将宏任务下的代码块放入宏任务队列中,遇到微任务则将微任务微任务下的代码块放入微任务队列中,当所有同步代码执行完毕后,再将微任务从队列中调入主线程执行,微任务执行完毕后再将异步宏任务从队列中调入主线程执行,一直循环直至所有任务执行完毕。

注意:上面写的是将任务下的代码块放入队列,是因为开启异步任务的方法在调用时都是同步执行的,只是它们接收的方法是进入对应异步队列来排队执行。

下面我们来看点例子:

console.log('1')
setTimeout(() => {
    console.log('2')
})
Promise.resolve().then(() => {
    console.log('3')
})
console.log('4')

分析:

1. 首先碰到代码块,开启一个宏任务从,并且没有其他代码块了,执行完毕,检查微任务队列,发现没有任务
2. 从宏任务队列取出第一个宏任务代码块,并执行
3. 来到第一行,执行1
4. 往下碰到setTimeout开启一个宏任务,将其包含的代码块放入宏任务队列
5. 再往下碰到promise.then()开启一个微任务,将其包含的代码块放入微任务队列
6. 到最后一行执行4
7. 宏任务执行完毕,检查微任务队列,发现微任务存在任务,依次执行微任务
8. 执行3,微任务队列执行完毕
9. 再从宏任务队列取出一个代码块,执行
10. 执行2,宏任务执行完毕,且检查后发现微任务队列没有任务
11. 检查宏任务队列,发现宏任务队列没有任务,代码执行完毕

结果是:1、4、3、2

然后我们来看一个稍微复杂一点的例子:

console.log(1)
Promise.resolve().then(() => {
    console.log(2)
})
setTimeout(() => {
    console.log(3)
    new Promise((resolve) => {
        setTimeout(() => {
            console.log(4)
            resolve()
        }, 1000)
    }).then(() => {
        console.log(5)
        setTimeout(() => {
            console.log(6)
            Promise.resolve().then(() => {
                console.log(7)
            })
        }, 1000)
        Promise.resolve().then(() => {
            console.log(8)
        })
        Promise.resolve().then(() => {
            console.log(9)
        })
    })
    setTimeout(() => {
        console.log(10)
    }, 1000)
}, 0)

答案是:1,2,3,4,5,8,9,10,6,7 解析:

宏任务微任务.png

关于setTimeout

如果按照上面事件循环的规律来看下面这段代码,请问是该输出什么呢

宏任务微任务setTimeout1.png

很明显是1,2吧,但是再看下面这段代码:

宏任务微任务setTimeout.png

先说答案,2,1,这是一个意料之中的答案,但是在我查资料的时候发现很多地方的描述是setTimeout是一个宏任务,然后我就觉得这样不是自相矛盾了吗,宏任务是需要先执行完前一个再执行后一个,于是我在前面特意强调了是异步任务下的代码块放入对应队列,此处如果有更好表述方式欢迎指正(实在是不知道该怎么形容,叫callback好像也不太好)。

总结

宏任务先执行,每执行完一个宏任务之后会去检查微任务队列,并且将微任务队列里的任务全部执行,执行完之后再执行下一个宏任务。注意,整个js代码再开始执行的时候就创建了一个宏任务,所以如果没考虑到这点,那么最开始看起来会像是微任务比宏任务先执行。

参考

HTML Standard (whatwg.org)

任务、微任务、队列和计划 - JakeArchibald.com