浅谈JS宏任务与微任务

188 阅读5分钟
  1. 背景知识

JavaScript 是一个单线程的脚本语言, 同步异步任务执行有先后顺序之分

  1. 什么是宏任务和微任务

宏任务

(macro)task,可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)。

浏览器为了能够使得 JS 内部(macro)task 与 DOM 任务能够有序的执行,会在一个(macro)task 执行结束后,在下一个(macro)task 执行开始前,对页面进行重新渲染,流程如下:

(macro)task->渲染->(macro)task->...
第一轮宏任务
查看异步任务,丢进第二轮宏任务队列(只是放进去了)
微任务
刷新
第二轮开始

微任务

microtask,可以理解是在当前 task 执行结束后立即执行的任务。也就是说,在当前 task 任务后,下一个 task 之前,在渲染之前。

所以它的响应速度相比 setTimeout(setTimeout 是 macrotask)会更快,因为无需等渲染。也就是说,在某一个 macrotask 执行完后,就会将在它执行期间产生的所有 microtask 都执行完毕(在渲染前)。

  1. 哪些是宏任务与微任务

宏任务包含

script(整体代码)
setTimeout
setInterval
I/O
UI交互事件
postMessage
MessageChannel
setImmediate(Node.js 环境)

微任务包含

Promise.then
Object.observe
MutationObserver
process.nextTick(Node.js 环境)
  1. 运行机制
  • 执行一个宏任务(栈中没有就从事件队列中获取)
  • 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
  • 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
  • 当前宏任务执行完毕,开始检查渲染,然后 GUI 线程接管渲染
  • 渲染完毕后,JS 线程继续接管,开始下一个宏任务(从事件队列中获取)
  1. 实例
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 那一套有区别。