学习总结 Event Loop、宏任务和微任务

232 阅读6分钟

晓畅 Event Loop、宏任务和微任务

在进入主题之前,得先了解JavaScript这门语言。它最大的一个特点就是单线程,也就是说,同一时间只能做一件事。

为什么js是单线程?

js作为主要运行在浏览器的脚本语言,js主要用途之一是操作DOM。 如果js同时有两个线程,同时对同一个dom进行操作,这时浏览器应该听哪个线程的,如何判断优先级? 为了避免这种问题,js必须是一门单线程语言,并且在未来这个特点也不会改变。

执行栈

当执行某个函数、用户点击一次鼠标,Ajax完成,一个图片加载完成等事件发生时,只要指定过回调函数,这些事件发生时就会进入执行栈队列中,等待主线程读取,遵循先进先出原则。

主线程

要明确的一点是,主线程跟执行栈是不同概念,主线程规定现在执行执行栈中的哪个事件。 主线程循环:即主线程会不停的从执行栈中读取事件,会执行完所有栈中的同步代码。

任务队列

所有的任务可以分成两种,一种是同步任务,一种是异步任务。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完,才能执行后一个任务;异步任务指的是,不进入主线程,而是进入任务队列的任务,只有任务队列通知主线程,某个异步任务可以执行了,该任务才能进入主线程执行(执行栈)。
当遇到一个异步事件后,并不会一直等待异步事件返回结果,而是会将这个事件挂在与执行栈不同的队列中,我们称之为任务队列(Task Queue)。

Event Loop

主线程从任务队列中读取事件,这个过程是循环不断的,所以整个的这种运行机制称为Event Loop(事件循环)。 为了更好的理解事件循环,上图片。
1F0B3B16.png1F0B3B16.png1F0B3B16.png JavaScript运行过程.png

宏任务(macrotask)和微任务(microtask)

宏任务、微任务有哪些?

宏任务:1. script(可以理解为外层同步代码) 2.setTimeout/setInterval 3. UI rendering/UI事件 4.postMessage,MessageChannel 5.setImmediate,I/O(node.js)
微任务:1.Promise.then 2.process.nextTick(node.js) 3.MutationObserve

宏任务、微任务是怎么执行的?

执行顺序:先执行同步代码,遇到异步宏任务则将异步宏任务放入宏任务队列中,遇到异步微任务则将异步微任务放入微任务队列中,当所有同步代码执行完毕后,再将异步微任务从队列中调入主线程执行,微任务执行完毕后再将异步宏任务从队列中调入主线程执行,一直循环直至所有任务执行完毕。
总结:宏中有微,先微再宏。遇到await,后续为微。

围观下宏任务、微任务的详细执行过程吧!

下面这个例子,开始推理,会以为,先执行掉所有的同步代码,也就是打印1、2、4,然后执行.then这个异步微任务,但是,由于还没执行setTimeout,所以不会执行.then,然后直接打印timemerStart、timeEnd,然后就结束了。 正确的应该是执行完同步代码后,then没执行是因为promise内部的状态还是pending,此时去执行异步宏任务,当执行resolve后promise的状态变为了resolved,此时会将之前的promise.then推入到‘当前次’的微任务

const promise = new Promise((resolve, reject) => {
    console.log(1);
    
    setTimeout(() => {  //  异步宏任务
        console.log('timemerStart');
        resolve('success')  //  将状态变更到resolved,并且将之前的promise.then推入到当前次微任务
        console.log('timeEnd');
    },0)
​
    console.log(2);
})
​
promise.then(res => {
    console.log(res);
})
​
console.log(4);
// 1
// 2
// 4
// timemerStart
// timeEnd
// success

在没微任务的情况下,解释事件循环。第一次循环:最外层,同步代码打印'start',无微任务执行宏任务,两次宏任务分别打印timer1和timer2,这两次相当于第二次的同步代码,所以接下来是进入第二次事件循环:无微任务,执行宏任务,打印timer3。在这里总结一点,每次循环的宏任务可以说是下一次事件循环的开始(同步代码执行)。

setTimeout(() => {
    console.log('timer1');
    setTimeout(() => {
        console.log('timer3');
    },0)
}, 0)
​
setTimeout(() => {
    console.log('timer2');
},0)
​
console.log('start');
// start
// timer1
// timer2
// timer3

上面的宏任务、微任务的解释可能会听起来没那么容易理解,下面是一段多循环执行代码

console.log('1');
​
setTimeout(function () {  //  setTimeout1
    console.log('2');
    process.nextTick(function () { // process2
        console.log('3');
    })
    new Promise(function (resolve) {
        console.log('4');
        resolve();
    }).then(function () { // then2
        console.log('5')
    })
})
process.nextTick(function () { // process1
    console.log('6');
})
new Promise(function (resolve) {
    console.log('7');
    resolve();
}).then(function () { //  then1
    console.log('8')
})
​
setTimeout(function () { // setTimeout2
    console.log('9');
    process.nextTick(function () {  // process3
        console.log('10');
    })
    new Promise(function (resolve) {
        console.log('11');
        resolve();
    }).then(function () { // then3
        console.log('12')
    })
})

第一轮循环

1)、首先打印 1
2)、接下来是setTimeout是异步任务且是宏任务,加入宏任务暂且记为 setTimeout1
3)、接下来是 process 微任务 加入微任务队列 记为 process1
4)、接下来是 new Promise 里面直接 打印 7 后面的then是微任务 记为 then1
5)、setTimeout 宏任务 记为 setTimeout2

第一轮循环打印出的是 1 7
当前宏任务队列:setTimeout1, setTimeout2
当前微任务队列:process1, then1,

第二轮循环

1)、执行所有微任务
2)、执行process1,打印出 6
3)、执行then1 打印出8
4)、微任务都执行结束了,开始执行第一个宏任务
5)、执行 setTimeout1
6)、首先打印出 2
7)、遇到 process 微任务 记为 process2
8)、new Promise中resolve 打印出 4
9)、then 微任务 记为 then2

第二轮循环结束,当前打印出来的是 1 7 6 8 2 4
当前宏任务队列:setTimeout2
当前微任务队列:process2, then2

第三轮循环

1)、执行所有的微任务
2)、执行 process2 打印出 3
3)、执行 then2 打印出 5
4)、执行第一个宏任务,也就是执行 setTimeout2
5)、首先打印出 9
6)、process 微任务 记为 process3
7)、new Promise执行resolve 打印出 11
8)、then 微任务 记为 then3

第三轮循环结束,当前打印顺序为:1 7 6 8 2 4 3 5 9 11
当前宏任务队列为空
当前微任务队列:process3,then3

第四轮循环

1)、执行所有的微任务
2)、执行process3 打印出 10
3)、执行then3 打印出 12

代码执行结束: 最终打印顺序为:1 7 6 8 2 4 3 5 9 11 10 12

总结

先同步后异步,异步中先微后宏,宏任务中又会有同步和异步,所以可以说,宏任务的开始其实也意味着下一次循环的开始。