JS执行顺序——宏任务&微任务

2,140 阅读4分钟

JS运行机制

概念1: JS是单线程执行

“JS是单线程的”指的是JS引擎线程。

在浏览器环境中,有JS 引擎线程和渲染线程,且两个线程互斥。

Node环境中,只有JS 线程。

概念2:宿主

JS运行的环境。一般为浏览器或者Node。

概念3:执行栈

是一个存储函数调用的栈结构,遵循先进后出的原则。

概念4:Event Loop

JS到底是怎么运行的呢?

JS引擎常驻于内存中,等待宿主将JS代码或函数传递给它。 也就是等待宿主环境分配宏观任务,反复等待-执行即为事件循环。

Event Loop中,每一次循环称为tick,每一次tick的任务如下:

  • 执行栈选择最先进入队列的宏任务(一般都是script),执行其同步代码直至结束;
  • 检查是否存在微任务,有则会执行至微任务队列为空;
  • 如果宿主为浏览器,可能会渲染页面;
  • 开始下一轮tick,执行宏任务中的异步代码(setTimeout等回调)。

概念5:宏任务和微任务

ES6 规范中,microtask 称为 jobs,macrotask 称为 task 宏任务是由宿主发起的,而微任务由JavaScript自身发起。

在ES3以及以前的版本中,JavaScript本身没有发起异步请求的能力,也就没有微任务的存在。在ES5之后,JavaScript引入了Promise,这样,不需要浏览器,JavaScript引擎自身也能够发起异步任务了。

详细概念

微任务和宏任务是异步任务的两个分类。

宏任务:当前调用栈中执行的代码成为宏任务。(主代码块,定时器等)。

微任务:当前(此次事件循环中)宏任务执行完,在下一个宏任务开始之前需要执行的任务,可以理解为回调事件。(Promise.then,proness.nextTick 等等)。

宏任务中的事件放在 callback queue 中,由事件触发线程维护;微任务的事件放在微任务队列中,由 js 引擎线程维护。

在挂起任务时,JS 引擎会将所有任务按照类别分到这两个队伍中,首先在宏任务的队列中取出第一个任务,执行完毕后,取出微任务队列中的所有任务顺序执行;之后再取出宏任务,周而复始,直到两个队列的任务都取完。

其实并不止一个消息队列,有异步队列和事件队列,而事件队列总是优先于异步队列被空闲下来的JS线程取用。

宏任务一般是:包括整体代码script,setTimeout,setInterval、I/O、UI render。

微任务主要是:Promise、Object.observe、MutationObserver。

  • 第一步,先按同步代码顺序运行
  • 第二步,开始清空微任务队列
  • 第三步,开始清空宏任务队列(执行一个宏任务,把相关微任务添加入微任务队列)
  • 第四步:开始清空微任务队列(上一个执行宏任务中加入队列的微任务一次性全部执行完成)
  • 第五步:开始清空宏任务队列(执行下一个宏任务,把相关微任务添加入微任务队列)

..........循环一直到执行完成

所以,总结一下,两者区别为:

宏任务(macrotask)微任务(microtask)
谁发起的宿主(Node、浏览器)JS引擎
具体事件1. script (可以理解为外层同步代码) 2.setTimeout/setInterval 3. UI rendering/UI事件 4. postMessage,MessageChannel 5. setImmediate,I/O(Node.js)1. Promise 2. MutaionObserver 3. Object.observe(已废弃;Proxy 对象替代) 4. process.nextTick(Node.js)
谁先运行后运行先运行
会触发新一轮Tick吗不会

应用场景 - Vue中的vm.$nextTick

vm.$nextTick 接受一个回调函数作为参数,用于将回调延迟到下次DOM更新周期之后执行。

这个API就是基于事件循环实现的。 “下次DOM更新周期”的意思就是下次微任务执行时更新DOM,而vm.$nextTick就是将回调函数添加到微任务中(在特殊情况下会降级为宏任务)。

因为微任务优先级太高,Vue 2.4版本之后,提供了强制使用宏任务的方法。

vm.$nextTick优先使用Promise,创建微任务。 如果不支持Promise或者强制开启宏任务,那么,会按照如下顺序发起宏任务:

  1. 优先检测是否支持原生 setImmediate(这是一个高版本 IE 和 Edge 才支持的特性)
  2. 如果不支持,再去检测是否支持原生的MessageChannel
  3. 如果也不支持的话就会降级为 setTimeout

几个执行顺序的典型例子

举例1

console.log('script start')

async function async1() {
  await async2()
  console.log('async1 end')
}
async function async2() {
  console.log('async2 end')
}
async1()

setTimeout(function() {
  console.log('setTimeout')
}, 0)

new Promise(resolve => {
  console.log('Promise')
  resolve()
})
  .then(function() {
    console.log('promise1')
  })
  .then(function() {
    console.log('promise2')
  })

console.log('script end')

答案

script start
async2 end
Promise
script end
async1 end
promise1
promise2
setTimeout

举例2

console.log('sync statement 1');
Promise.resolve().then(function() {
    console.log('micro task 1');
    setTimeout(function() {
        console.log('macro task 1');
    }, 0);
}).then(function() {
    console.log('micro task 2');
});
 
setTimeout(function() {
    console.log('macro task 2')
    Promise.resolve().then(function(){
        console.log('micro task 3');
    })
}, 0)
console.log('sync statement 2'); 

答案

sync statement 1
sync statement 2
micro task 1
micro task 2
macro task 2
micro task 3
macro task 1

举例3

console.log('1');
setTimeout(function () {
    console.log('2');
    new Promise(function (resolve) {
        console.log('3');
        resolve();
    }).then(function () {
        console.log('4');
    })
}, 0);
new Promise(function (resolve) {
    console.log('5');
    resolve();
}).then(function () {
    console.log('6');
});
setTimeout(function () {
    console.log('7');
    new Promise(function (resolve) {
        console.log('8');
        resolve();
    }).then(function () {
        console.log('9');
    });
})

答案

1
5
6
2
3
4
7
8
9