- 背景知识
JavaScript 是一个单线程的脚本语言, 同步异步任务执行有先后顺序之分
- 什么是宏任务和微任务
宏任务
(macro)task,可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)。
浏览器为了能够使得 JS 内部(macro)task 与 DOM 任务能够有序的执行,会在一个(macro)task 执行结束后,在下一个(macro)task 执行开始前,对页面进行重新渲染,流程如下:
(macro)task->渲染->(macro)task->...
第一轮宏任务
查看异步任务,丢进第二轮宏任务队列(只是放进去了)
微任务
刷新
第二轮开始
微任务
microtask,可以理解是在当前 task 执行结束后立即执行的任务。也就是说,在当前 task 任务后,下一个 task 之前,在渲染之前。
所以它的响应速度相比 setTimeout(setTimeout 是 macrotask)会更快,因为无需等渲染。也就是说,在某一个 macrotask 执行完后,就会将在它执行期间产生的所有 microtask 都执行完毕(在渲染前)。
- 哪些是宏任务与微任务
宏任务包含
script(整体代码)
setTimeout
setInterval
I/O
UI交互事件
postMessage
MessageChannel
setImmediate(Node.js 环境)
微任务包含
Promise.then
Object.observe
MutationObserver
process.nextTick(Node.js 环境)
- 运行机制
- 执行一个宏任务(栈中没有就从事件队列中获取)
- 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
- 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
- 当前宏任务执行完毕,开始检查渲染,然后 GUI 线程接管渲染
- 渲染完毕后,JS 线程继续接管,开始下一个宏任务(从事件队列中获取)
- 实例
setTimeout(_ => console.log(4))
new Promise(resolve => {
resolve()
console.log(1)
}).then(_ => {
console.log(3)
})
console.log(2)
结果 : 1,2,3,4
setTimeout就是作为宏任务来存在的,而Promise.then则是具有代表性的微任务,上述代码的执行顺序就是按照序号来输出的。
所有会进入的异步都是指的事件回调中的那部分代码
也就是说new Promise在实例化的过程中所执行的代码都是同步进行的,而 then 中注册的回调才是异步执行的。
在同步代码执行完成后才回去检查是否有异步任务完成,并执行对应的回调,而微任务又会在宏任务之前执行。
+部分表示同步执行的代码
+setTimeout(_ => {
- console.log(4)
+})
+new Promise(resolve => {
+ resolve()
+ console.log(1)
+}).then(_ => {
- console.log(3)
+})
+console.log(2)
我们继续在Promise中实例化Promise,其输出依然会早于 setTimeout 的宏任务
setTimeout(_ => console.log(4))
new Promise(resolve => {
resolve()
console.log(1)
}).then(_ => {
console.log(3)
Promise.resolve().then(_ => {
console.log('before timeout')
}).then(_ => {
Promise.resolve().then(_ => {
console.log('also before timeout')
})
})
})
console.log(2)
结果 : 1, 2, 3, before timeout,also before timeout, 4
setTimeout(() => {
//执行后 回调一个宏事件
console.log('内层宏事件3')
}, 0)
console.log('外层宏事件1');
new Promise((resolve) => {
console.log('外层宏事件2');
resolve()
}).then(() => {
console.log('微事件1');
}).then(()=>{
console.log('微事件2')
})
打印结果
外层宏事件1
外层宏事件2
微事件1
微事件2
内层宏事件3
• 首先浏览器执行 js 进入第一个宏任务进入主线程, 遇到 setTimeout 分发到宏任务 Event Queue 中
• 遇到 console.log() 直接执行 输出 外层宏事件 1
• 遇到 Promise, new Promise 直接执行 输出 外层宏事件 2
• 执行 then 被分发到微任务 Event Queue 中
•第一轮宏任务执行结束,开始执行微任务 打印 '微事件 1' '微事件 2'
•第一轮微任务执行完毕,执行第二轮宏事件,打印 setTimeout 里面内容'内层宏事件 3'
/主线程直接执行
console.log('1');
//丢到宏事件队列中
setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
//微事件1
process.nextTick(function() {
console.log('6');
})
//主线程直接执行
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
//微事件2
console.log('8')
})
//丢到宏事件队列中
setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})
• 首先浏览器执行 js 进入第一个宏任务进入主线程, 直接打印 console.log('1')
• 遇到 setTimeout 分发到宏任务 Event Queue 中
• 遇到 process.nextTick 丢到微任务 Event Queue 中
• 遇到 Promise, new Promise 直接执行 输出 console.log('7');
• 执行 then 被分发到微任务 Event Queue 中
•第一轮宏任务执行结束,开始执行微任务 打印 6,8
•第一轮微任务执行完毕,执行第二轮宏事件,执行 setTimeout
•先执行主线程宏任务,在执行微任务,打印'2,4,3,5'
•在执行第二个 setTimeout,同理打印 ‘9,11,10,12’
•整段代码,共进行了三次事件循环,完整的输出为 1,7,6,8,2,4,3,5,9,11,10,12。
以上是在浏览器环境下执行的数据,只作为宏任务和微任务的分析,我在 node 环境下测试打印出来的顺序为:1,7,6,8,2,4,9,11,3,10,5,12。node 环境执行结果和浏览器执行结果不一致的原因是:浏览器的 Event loop 是在 HTML5 中定义的规范,而 node 中则由 libuv 库实现。libuv 库流程大体分为 6 个阶段:timers,I/O callbacks,idle、prepare,poll,check,close callbacks,和浏览器的 microtask,macrotask 那一套有区别。