js中的微任务与宏任务

266 阅读5分钟

JavaScript的一个特点就是单线程,意味着同一时间只能做一件事。
这导致了后面的任务就要等到前面的任务完成后才能执行,如果前面的任务耗时很长,就会导致后面的任务一直处于等待中,可能带来进程阻塞的问题。
为了解决这个问题,js有两种任务的执行模式:同步模式和异步模式。

1. 同步任务与异步任务

1.1 同步任务(Synchronous)

在主线程上排队执行的任务只有前一个任务执行完毕,才能在执行下一个任务,形成一个执行栈。

image.png

1.2 异步任务(Asynchronous)

异步任务分为宏任务和微任务。
不进入主线程,而是进入任务队列。主线程之外,事件触发线程管理着一个任务队列,只要异步任务有了运行结果,就在任务队列中放置一个事件。
执行栈中的所有同步任务执行完毕(此时js引擎空闲),系统就会读取任务队列,将可运行的异步任务添加到可执行栈中,开始执行。

image.png

image.png

image.png

2. 任务队列和事件循环

  1. 所有的同步任务都在主线程上执行,形成一个执行栈。
  2. 除了主线程之外,还存在一个任务队列,只要异步任务有了运行结果,就在任务队列中植入一个时间标记。
  3. 主线程完成所有任务(执行栈清空),就会读取任务队列,先执行微任务队列再执行宏任务队列。
  4. 重复以上三步。 只要主线程空了,就会读取任务队列,这就是js的运行机制,也被称为event loop(事件循环)

2.1 事件循环

由于主线程不断重复的获得任务、执行任务、再获取再执行,所以这种机制被叫做事件循环(Event Loop)。 Event Loop 包含两类:一类是基于 Browsing Context,一种是基于 Worker

2.2. 任务队列

根据规范,事件循环是通过任务队列的机制进行协调的。
一个Event Loop中,可以有一个或者多个任务队列(task queue),一个任务队列便是一系列有序任务(task)的集合
任务队列也分为宏任务队列微任务队列
每个任务都有一个任务源(task source),源自同一个任务源的 task 必须需放到同一个任务队列,从不同源来的则被添加到不同队列。setTimeout/Promise 等API便是任务源,而进入任务队列的是他们指定的具体执行任务。

在事件循环中,每进行一次循环操作称为 tick ,每一次tick的任务处理模型是比较复杂的,但关键步骤如下:

  • 在此次 tick 中选择最先进入队列的任务(oldest task),如果有则执行(一次)
  • 检查是否存在 Microtasks ,如果存在则不停地执行,直至清空Microtasks Queue
  • 更新 render
  • 主线程重复执行上述步骤

image.png

3. 宏任务与微任务

在异步模式下,创建异步任务主要分为宏任务和微任务两种。ES6规范中,宏任务(Macrotask)称为Task,微任务(Microtask)称为Jobs。宏任务是由宿主(浏览器、Node)发起的,而微任务由js自身发起。
注意:

  • 微任务比宏任务的执行时间要早。
  • 宏任务里如果有宏任务,不会执行里面的那个宏任务,而是被丢进任务队列后面,所以会最后执行。
  • 同步任务->微任务->宏任务

3.1 宏任务(Macrotask)

每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)。
浏览器为了能够使得js内部 macrotask 与 DOM 任务能够有序的执行,会在一个 macrotask 执行结束后,在下一个 macrotask 执行开始前,对页面进行重新渲染。 macrotask -> 渲染 -> macrotask

3.1.1 宏任务包含

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

3.2 微任务(Microtask)

在当前 task 执行结束后立即执行的任务。在当前 task 任务后,在下一个 task 之前,在渲染之前。task -> microtask -> 渲染 -> task
在某个 macrotask 执行完成后,就会将在它执行期间产生的所有 microtask 都执行完毕(在渲染之前)。

3.2.1 微任务包含

  • Promise.then
    • Promise 的状态决定 Promise.then 的执行顺序,若状态为 pending,执行靠后;若状态为 fulfilled ,执行靠前。(两种相对而言)
    • .then 返回一个非 Promise 对象,其执行时间(变更状态的时间)会快于返回一个 Promise 对象。
    • 参考:Promise和async/await题目解析
  • async/await
    • 执行完 await 修饰的对象后,先执行 async 外的同步代码,然后再回到原处继续执行。即遇到 await 就阻塞,执行完 async 外面的同步代码后,再回到内部
  • Object.observe
  • MutationObserver
  • process.nextTick(Node.js 环境)

3.3 任务执行机制

在事件循环中,每进行一次循环操作称为 tick ,每一次 tick 的任务处理模型是比较复杂的,但关键步骤如下:

  • 执行一个宏任务(栈中没有就从事件队列中获取)
  • 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
  • 宏任务执行完毕后,立即执行当前微任务队列的所有微任务(依次执行)
  • 当前宏任务执行完毕,开始检查渲染,然后 GUI 线程接管渲染
  • 渲染完毕后,js 线程继续接管,开始下一个宏任务(从事件队列中获取)

image.png

3.3.1 执行顺序练习题

搜集了部分题目,仅供参考,侵删 blog.csdn.net/m0_44973790…
注意: Promise.resolve()new Promise()都是实例化过程,同步执行

setTimeout(function(){
    console.log('setTimeout') 
},0) 		
console.log('script start')	
async function async1(){
    console.log('async1 start');
    await async2()
    console.log('async1 end')
}	
async function async2(){
    console.log('async2 end')
}	
new Promise(function(resolve){
    console.log('promise')
    for (var i = 0; i <10000; i++) {
        if (i === 10) {
            console.log("3")
        }
        i == 9999 && resolve('4')
    }
    resolve();
}).then(function(val){
    console.log(val)
    console.log('promise1')
}).then(function(res){
    console.log(res)
    console.log('promise2')
})	
async1(); 	
console.log('script end')

image.png

setTimeout(() => {//宏任务队列1
  console.log('1');//宏任务队1的任务1

  setTimeout(() => {//宏任务队列3(宏任务队列1中的宏任务)
    console.log('2')
  }, 0)

  new Promise(resolve => {
    resolve()
    console.log('3')//宏任务队列1的任务2
  }).then(() => {//宏任务队列1中的微任务
    console.log('4')
  })

}, 0)
 
setTimeout(() => {//宏任务队列2
  console.log('5')
}, 0)

console.log('6')//同步主线程

image.png

async function test1() {
console.log("test1 begin");
const result = await test2();
console.log("result", result);
console.log("test1 end");
}
async function test2() {
console.log("test2");
}
console.log("script begin");
test1();
console.log("script end");

image.png

console.log(1)

setTimeout(()=>{
    console.log(2)
    Promise.resolve().then(()=>{
        console.log(3)
    })
}

new Promise((resolve,reject)=>{
    console.log(4)
    resolve(5)
}).then((data)=>{
    console.log(data)
})

setTimeout(()=>{
    console.log(6)
})

console.log(7)

image.png