晓畅 Event Loop、宏任务和微任务
在进入主题之前,得先了解JavaScript这门语言。它最大的一个特点就是单线程,也就是说,同一时间只能做一件事。
为什么js是单线程?
js作为主要运行在浏览器的脚本语言,js主要用途之一是操作DOM。 如果js同时有两个线程,同时对同一个dom进行操作,这时浏览器应该听哪个线程的,如何判断优先级? 为了避免这种问题,js必须是一门单线程语言,并且在未来这个特点也不会改变。
执行栈
当执行某个函数、用户点击一次鼠标,Ajax完成,一个图片加载完成等事件发生时,只要指定过回调函数,这些事件发生时就会进入执行栈队列中,等待主线程读取,遵循先进先出原则。
主线程
要明确的一点是,主线程跟执行栈是不同概念,主线程规定现在执行执行栈中的哪个事件。 主线程循环:即主线程会不停的从执行栈中读取事件,会执行完所有栈中的同步代码。
任务队列
所有的任务可以分成两种,一种是同步任务,一种是异步任务。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完,才能执行后一个任务;异步任务指的是,不进入主线程,而是进入任务队列的任务,只有任务队列通知主线程,某个异步任务可以执行了,该任务才能进入主线程执行(执行栈)。
当遇到一个异步事件后,并不会一直等待异步事件返回结果,而是会将这个事件挂在与执行栈不同的队列中,我们称之为任务队列(Task Queue)。
Event Loop
主线程从任务队列中读取事件,这个过程是循环不断的,所以整个的这种运行机制称为Event Loop(事件循环)。 为了更好的理解事件循环,上图片。
![]()
宏任务(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
总结
先同步后异步,异步中先微后宏,宏任务中又会有同步和异步,所以可以说,宏任务的开始其实也意味着下一次循环的开始。