[javascript核心-07] 带你彻底弄懂事件循环机制、宏任务、微任务
本文github地址:JavaScript_Interview_Everything 大前端知识体系与面试宝典,从前端到后端,全栈工程师,成为六边形战士
1. 宏任务微任务与事件循环
1.1. 进程与线程
进程:计算机上所运行的程序,是操作系统管理程序的方式
线程:操作系统能够调度的最小单元
关系:进程需要至少启动一个线程执行代码,这个线程成为主线程,所以进程是线程的容器
javascript执行线程:我们说进程是线程的容器,浏览器或者 Node 环境就是js的容器进程,但js是单线程的
js是单线程的:这意味着js在一个时间片内只能执行一段程序,如果其中一段程序运行非常耗时,则会阻塞线程,因此类似网络请求等操作不交由js线程处理,而是用回调函数交由其他线程处理,何时执行回调函数?这里就要用到事件队列与事件循环机制;多线程有资源抢占的问题
浏览器进程:是多进程的,浏览器本身有多个进程;新开一个tab页面就会开启一个新的进程,如果一个网页崩溃卡死就只影响这单个进程,不会导致所有页面无法响应而需要整个浏览器强制退出
1.2. 浏览器线程
每个浏览器进程都有多个线程,而处理js的只是其中一个线程。而显示一个页面除了解析 JS 还有许多其他工作,因此需要多个线程完成不同的任务:
GUI 渲染进程:渲染和解析页面
JS 引擎线程:渲染和解析 js(只分配一个线程执行js)
定时器监听线程:监听定时器
事件监听线程:监听和处理事件绑定操作
网络请求线程:获取服务器数据和资源(同源下最多同时分配6个TCP线程)
webwork线程
1.3. 同步与异步
由于 JS 是单线程的,因此 JS 的代码大部分都是同步代码。前面的代码不执行结束,不会往下执行。因此死循环和无限递归在 JS 中是致命的。
但是 JS 必然是存在异步执行的代码,此时就必须交由其他线程去执行,而多个异步操作应该按什么顺序执行,以及如何获取执行结果,就需要专门的机制来管理,在 JS中这个机制就是事件循环机制。
JS 中的异步操作是借用浏览器的多线程机制,基于 EventLoop 事件循环机制实现单线程异步操作。
1.4. 浏览器事件循环机制
概述:js主线程将耗时操作交由其他线程处理,其他线程会将这些操作放入事件循环队列,当耗时任务得到结果后,会交由js主线程执行
浏览器加载页面时会为当前页面开启两个队列:
WebAPI 任务监听队列:监听异步队列是否可以执行
EventQueue 事件队列:所有可执行的异步任务需要在该队列中入队等待
代码中遇到异步代码,先放入WebAPI 任务监听队列,浏览器使用新的线程进行监听,然后继续向下执行同步代码;当监听到异步操作可以执行后,根据微任务和宏任务将其放入EventQueue 的不同事件队列进行入队等待。
特别的,对于定时器来说,当设定时间到了之后也不会立即执行,是会先放入事件队列,排队执行。
当同步代码都执行完毕之后,主线程空闲,开始从EventQueue按照顺序取出异步任务放入执行栈中,由主线程执行,执行完一个异步任务再取出下一个执行。
执行顺序:相对宏任务队列,优先执行微任务队列中的任务;对于相同类型的异步任务,按照进队顺序逐一出队执行。即在执行任何宏任务之前,要先保证清空微任务队列
宏任务队列:定时器、网络请求、DOM 回调(事件绑定)、UI Rendering、消息队列(MessageChannel)
微任务队列:普通回调函数、Promise.then()、async/await、MutationObserver API(监听当前 dom 元素的属性值改变)、requestAnimationFrame、IntersectionObserver(监听当前dom元素和可视窗口交叉信息发生改变)
1.5. 浏览器事件循环机制代码分析
setTimeout(()=>{
    console.log(1)
},20)
console.log(2)
setTimeout(()=>{
    console.log(3)
},10)
console.log(4)
for(let i=0;i<90000000;i++){
}
console.log(5)
setTimeout(()=>{
    console.log(6)
},8)
console.log(7)
setTimeout(()=>{
    console.log(8)
},15)
console.log(9)
- 将定时器1放入 WebAPI任务监听队列,由浏览器分配一个定时器监听线程,进行计时监听
setTimeout(()=>{
    console.log(1)
},20)
- 直接执行同步代码console.log(2)输出2
- 同样将定时器2放入 WebAPI任务监听队列,由浏览器分配一个定时器监听线程,进行计时监听
setTimeout(()=>{
    console.log(3)
},10)
- 直接执行同步代码console.log(4)输出4
- 执行同步循环任务,该循环耗时较长,大概需要 70~80ms(根据硬件差异),此时定时器1 先计时完毕,放入EventQueue中的宏任务队列中。然后定时器2 先计时完毕,放入EventQueue中的宏任务队列中。需要等待该同步循环执行完毕再往下执行。
for(let i=0;i<90000000;i++){
}
- 直接执行同步代码console.log(5)输出5
- 将该定时器3放入 WebAPI任务监听队列,由浏览器分配一个定时器监听线程,进行计时监听,需要等待 8 毫秒
setTimeout(()=>{
    console.log(6)
},8)
- 直接执行同步代码console.log(7)输出7
- 将该定时器4放入 WebAPI任务监听队列,由浏览器分配一个定时器监听线程,进行计时监听,需要等待 15 毫秒
setTimeout(()=>{
    console.log(8)
},15)
同步代码执行完毕,开始执行异步代码:
- 查看EventQueue中的微任务队列,当前没有需要执行的微任务
- 查看EventQueue中的宏任务队列,取出定时器1 并执行
- 查看EventQueue中的微任务队列,当前没有需要执行的微任务
- 查看EventQueue中的宏任务队列,取出定时器2 并执行
- 主线程再次空闲,查看EventQueue中的微任务队列,当前没有需要执行的微任务
- 查看EventQueue中的宏任务队列,当前没有需要执行的宏任务,继续轮询等待
- 等待8ms后(定时器 3 的计时时间),定时器 3 被放入EventQueue中的宏任务队列
- 主线程轮询微任务队列、宏任务队列,取出定时器3 并执行
- 再次等待2ms后(定时器 4 计时10ms),定时器 3 被放入EventQueue中的宏任务队列
- 主线程轮询微任务队列、宏任务队列,取出定时器4 并执行
1.6. 异步函数 async代码执行分析
- async函数中的代码是同步执行的
- 遇到await会立即该语句下面的代码,作为微任务放入WebAPI队列中进行监听,等待前面await语句的返回结果。只有await返回的Promise状态是成功(resolve)的,才会放入事件队列EventQueue中
- 而当前上下文中,await语句的返回结果是一个 Promise 实例,即使返回的不是Promise,也会包装为 Promise返回。
- 同步代码执行完毕后,再从事件队列中取出微任务执行。
const fn = async()=>{
    console.log(1)
    return 10
}
(async function(){
    let result = await fn()
    console.log(2,result)
})()
console.log(3)
- await fn()函数直接执行,- console.log(1)直接打印出 1。由于前面是- await,因此- fn函数执行的返回值会被包装为- Promise返回,即- new Promise.resolve(1)
- console.log(2,result)作为微任务放入- WebAPI队列中进行监听,上面的- await语句返回结果为成功状态,因此可以执行,放入事件队列- EventQueue中执行
- console.log(3)输出3
- 从事件队列EventQueue中取出微任务进行执行,输出2,10
1.7. 异步函数与 Promise 的面试题
1.async函数中的代码是同步执行的,包括await语句也是同步执行
2.await后面的代码会被放入微任务队列
//02.js
async function async1 () {
  console.log('async1 start')
  await async2();
  console.log('async1 end')
}
 
async function async2 () {
  console.log('async2')
}
console.log('script start')
 
setTimeout(function () {
  console.log('setTimeout')
}, 0)
 
async1();
 
new Promise (function (resolve) {
  console.log('promise1')
  resolve();
}).then (function () {
  console.log('promise2')
})
console.log('script end')
// script start
// async1 start
// async2
// promsie1
// script end
// async1 end
// promise2
// setTimeout
- console.log('script start')同步代码直接执行
- 定时器1 直接放入webAPI队列中进行定时监听
setTimeout(function () {
  console.log('setTimeout')
}, 0)
- async1();执行,- console.log('async1 start')同步代码直接执行输出;- await async2(),后面的代码- console.log('async1 end')直接放入- webAPI队列中进行监听,为了方便先命令为微任务 1,它要等待前面- awiat的执行结果。
- 前面await async2()执行。async2()执行输出async2,无返回值,默认返回undefined,将其包装为promise实例,且是成功的状态,因此后面放入webAPI队列中微任务 1被放入微任务队列中等待 JS 轮询执行。
- 对于下面的代码,console.log('promise1')先同步执行。将then中的代码作为微任务放入webAPI队列中监听,将其命名为微任务 2。但是前面promise返回的结果是成功状态,因此将微任务 2放入微任务队列中等待 JS 轮询执行。
new Promise (function (resolve) {
  console.log('promise1')
  resolve();
}).then (function () {
  console.log('promise2')
})
- 同步执行代码console.log('script end')
此时同步代码已执行结果,目前的输出为
/*
script start
async1 start
async2
promise1
script end
*/
开始执行异步代码:
- 先按顺序执行微任务队列中的代码,取出微任务 1执行,console.log('async1 end'),输出结果为async1 end
- 取出微任务 2执行,console.log('promise2'),输出结果为promise2
- 微任务队列已清空,开始执行宏任务队列。
- 将定时完毕的定时器从webAPI队列中放入宏任务队列中,并取出执行,打印出setTimeout
此时异步代码执行完毕,异步代码的执行顺序为:
/*
async1 end
promise2
setTimeout
*/
总体执行顺序为:
// script start
// async1 start
// async2
// promsie1
// script end
// async1 end
// promise2
// setTimeout
1.8. 事件绑定与异步执行代码分析
注意:下面的代码中必须给html,body设置高度,不然点击无效。
<html lang="en">
<head>
    <style>
        html,body{
            height:100%;
        }
    </style>
</head>
<body>
    <script>
        let body = document.body;
        body.addEventListener('click', function(){
            Promise.resolve().then(()=>{
                console.log(1)
            })
            console.log(2)
        })
        body.addEventListener('click', function(){
            Promise.resolve().then(()=>{
                console.log(3)
            })
            console.log(4)
        })
    </script>
</body>
</html>
- 事件绑定放入宏任务队列,命名为宏任务1。其点击事件放入webAPI监听事件触发,这里的事件触发为click点击事件
body.addEventListener('click', function(){
    Promise.resolve().then(()=>{
        console.log(1)
    })
    console.log(2)
})
- 事件绑定放入宏任务队列,命名为宏任务2。其点击事件放入webAPI监听事件触发,这里的事件触发为click点击事件
body.addEventListener('click', function(){
    Promise.resolve().then(()=>{
        console.log(3)
    })
    console.log(4)
})
- 点击body,宏任务1和宏任务2都可以开始执行。按照绑定先后顺序放入宏任务队列中
- 宏任务1先执行。将then(()=>{console.log(1)})放入webAPI监听直到前面的返回状态成功。前面返回为成功状态,因此放入微任务队列,命名为微任务1。下面的同步代码直接输入2
    Promise.resolve().then(()=>{
        console.log(1)
    })
    console.log(2)
- JS 轮询微任务队列,发现微任务队列中有微任务1。先执行微任务1,输出 1
- JS 轮询宏任务队列。将then(()=>{console.log(3)})放入webAPI监听直到前面的返回状态成功。前面返回为成功状态,因此放入微任务队列,命名为微任务2。下面的同步代码直接输入4
- JS 轮询微任务队列,发现微任务队列中有微任务2。先执行微任务2,输出 3
1.9. Node 事件循环
Node 是多线程的,开启一个js线程处理js代码,其他线程处理耗时操作,将处理完的耗时操作将回调函数加入到事件队列,js通过轮询事件队列得到结果
Node 中的事件循环是通过libuv实现的,js本身无法执行的操作通过libuv进行;事件循环是js与系统调用之间的桥梁,文件 IO,数据库读取,网络 IO,定时器,子进程等操作都会在完成对应的操作后都会将对应的结果和回调函数放到事件循环(任务队列)中;事件循环会通过轮询机制不断的从任务队列中取出对应的事件(回调函数)执行
事件循环的阶段划分:
一次完成的 Tick 包括
1.定时器(Timer):执行定时器等函数
2.待定回调(Pending Callback):对某些系统操作执行回调
3.idle, prepare:系统内部使用
4.轮询(Poll):检查新的 I/O 操作;执行与 I/O 相关的回调
5.检测(check):setImmediate()执行
6.关闭的回调函数
1.10. Node 中的宏任务与微任务
宏任务: time queue:定时器
poll queue:IO
check queue:setImmediate()
close queue:close事件
微任务: next tick queue: process.nextTick() other queue:Promise.then()、普通回调
执行顺序:执行任何宏任务之前,要先保证清空微任务队列 nexttick queue
other queue
time queue:定时器
poll queue:IO
check queue:setImmediate()
close queue:close事件
1.11. 浏览器宏任务微任务执行顺序面试题
//01.js
setTimeout(function () {
  console.log("setTimeout1");
  new Promise(function (resolve) {
    resolve();
  }).then(function () {
    new Promise(function (resolve) {
      resolve();
    }).then(function () {
      console.log("then4");
    });
    console.log("then2");
  });
});
new Promise(function (resolve) {
  console.log("promise1");
  resolve();
}).then(function () {
  console.log("then1");
});
setTimeout(function () {
  console.log("setTimeout2");
});
console.log(2);
queueMicrotask(() => {
  console.log("queueMicrotask1")
});
new Promise(function (resolve) {
  resolve();
}).then(function () {
  console.log("then3");
});
// promise1
// 2
// then1
// queueMicrotask1
// then3
// setTimeout1
// then2
// then4
// setTimeout2
then()返回不同类型值的情况
1.直接返回普通值
//03.js
Promise.resolve().then(() => {
    console.log(0);
    // 相等于直接返回resolve(4)
    return 4 
  }).then((res) => {
    console.log(res)
  })
  
  
  Promise.resolve().then(() => {
    console.log(1);
  }).then(() => {
    console.log(2);
  }).then(() => {
    console.log(3);
  }).then(() => {
    console.log(5);
  }).then(() =>{
    console.log(6);
  })
  
/*
0
1
4
2
3
5
6
*/
2.返回一个实现了then()方法的对象
//04.js
Promise.resolve().then(() => {
    console.log(0);
    // 返回的是一个thenable的,多推一次微任务
    return {
        then : function(resolve){
            resolve(4)
        }
    }
  }).then((res) => {
    console.log(res)
  })
  
  
  Promise.resolve().then(() => {
    console.log(1);
  }).then(() => {
    console.log(2);
  }).then(() => {
    console.log(3);
  }).then(() => {
    console.log(5);
  }).then(() =>{
    console.log(6);
  })
/*
0
1
2
4
3
5
6
*/
  
3.thenable对象实现的then方法是一个宏任务
//05.js
Promise.resolve().then(() => {
    console.log(0);
    // thenable对象实现的then方法是一个宏任务
    return {
        then : function(resolve){
                setTimeout(function(){
                    resolve(4)
            })
        }
    }
  }).then((res) => {
    console.log(res)
  })
  
  
  Promise.resolve().then(() => {
    console.log(1);
  }).then(() => {
    console.log(2);
  }).then(() => {
    console.log(3);
  }).then(() => {
    console.log(5);
  }).then(() =>{
    console.log(6);
  })
/*
0
1
2
3
5
6
4
*/  
4.返回一个 Promise 对象
//06.js
Promise.resolve().then(() => {
    console.log(0);
    // 返回 Promise
    // 先将 Promise.resolve(4)向微任务向后推一次
    // 再将 Promise.resolve(4).then()向微任务向后再推一次
    return Promise.resolve(4)
  }).then((res) => {
    console.log(res)
  })
  
  
  Promise.resolve().then(() => {
    console.log(1);
  }).then(() => {
    console.log(2);
  }).then(() => {
    console.log(3);
  }).then(() => {
    console.log(5);
  }).then(() =>{
    console.log(6);
  })
/*
0
1
2
3
4
5
6
*/
1.12. Node 宏任务微任务执行顺序面试题
Node 微任务有两个队列, nexttick queue 与 other queue
有定时器的宏任务会在计时结束后加入到宏任务队列,但是这取决于计时时间
下面的第二个计时器是 300ms,会在最后执行,但如果将时间改为 1ms,它会比 setImmediate 更早执行,在倒数第二个打印
// 07.js
async function async1() {
    console.log('async1 start')
    await async2()
    console.log('async1 end')
  }
  
  async function async2() {
    console.log('async2')
  }
  
  console.log('script start')
  
  setTimeout(function () {
    console.log('setTimeout0')
  }, 0)
  
  setTimeout(function () {
    console.log('setTimeout2')
  }, 300)
  
  setImmediate(() => console.log('setImmediate'));
  
  process.nextTick(() => console.log('nextTick1'));
  
  async1();
  
  process.nextTick(() => console.log('nextTick2'));
  
  new Promise(function (resolve) {
    console.log('promise1')
    resolve();
    console.log('promise2')
  }).then(function () {
    console.log('promise3')
  })
  
  console.log('script end')
/*
script start
async1 start
async2
promise1
promise2
script end
nextTick1
nextTick2
async1 end
promise3
setTimeout0
setImmediate
setTimeout2
*/
本文github地址:JavaScript_Interview_Everything 大前端知识体系与面试宝典,从前端到后端,全栈工程师,成为六边形战士
转载自:https://juejin.cn/post/7241386277928550455




