事件循环机制 event loop
前言
事件循环是js执行的一个机制,理解了他就可以更好的理解js函数是如何执行的,对理解异步任务很有帮助!
浏览器执行线程
在解释事件循环之前首先先解释一下浏览器的执行线程:
浏览器是多进程的,浏览器每一个 tab 标签都代表一个独立的进程,其中浏览器渲染进程(浏览器内核)属于浏览器多进程中的一种,主要负责页面渲染,脚本执行,事件处理等
其包含的线程有:GUI 渲染线程(负责渲染页面,解析 HTML,CSS 构成 DOM 树)、JS 引擎线程、事件触发线程、定时器触发线程、http 请求线程等主要线程
执行栈和主线程
执行栈:
当执行某个函数、用户点击一次鼠标、Ajax请求完成、一个图片加载完成等事件发生时,只要指定了回调函数,这些事件发生时就会进入执行栈队列中,等待主线程读取,并遵循先进先出原则。
主线程:
js 引擎执行的线程,这个线程只有一个,页面渲染、函数处理都在这个主线程上执行。
要明确的一点是,主线程跟执行栈是不同概念,任务会被推入执行栈被主线程执行。
js任务分类
由于JavaScript是一种单线程的编程语言,因此JavaScript中的所有任务都需要排队依次完成。但这样的设计明显会有很大的一个问题,那就是如果碰到一个需要耗费很多的时间完成的事件时,很有可能会造成线程的阻塞问题。因此,JavaScript的开发者就将所有的任务分为两种来解决这种问题:
- 同步任务
- 异步任务
- 宏任务
- 微任务
宏任务&微任务
异步任务又被分为宏任务和微任务。
宏任务 (macro task):
- 创建主文档对象、解析 HTML、执行主线(或全局)JavaScript代码,更改当前 URL 以及各种事件,如页面加载、输入、网络事件和定时器事件。
- 从浏览器的角度来看,宏任务代表一个个离散的、独立工作单元。运行完一个宏任务之后,浏览器可以继续其他调度,如重新渲染页面的 UI 或执行垃圾回收。
常见宏任务:【script(整体代码) ,setTimeout,setInterval,I/O UI交互事件,postMessage MessageChannel setImmediate(Node.js 环境)】
微任务 (micro task):
- 微任务更新应用程序的状态,但必须在浏览器任务继续执行其他任务之前执行,浏览器任务包括重新渲染页面的UI。
- 微任务的案例包括 promise、回调函数、DOM 发生变化等。
- 微任务需要尽可能快地、通过异步方式执行,同时不能产生全新的微任务。微任务使得我们能够在重新渲染UI之前执行指定的行为,避免不必要的UI重绘,UI重绘会使应用程序的状态不连续。
常见微任务:【Promise.then ,Object.observe,MutaionObserver,process.nextTick(Node.js 环境)】
事件循环
事件循环的实现至少应该包含有一个用于宏任务的队列和一个用于微任务的队列。这使得事件循环能够根据任务类型进行优先处理。
总结起来也就是以下几步:
- 宏任务队列中,按照入队顺序,找到第一个执行的宏任务,放入调用栈,开始执行。
- 同步任务直接进入执行栈执行。
- 遇到一个异步任务:
- 如果是微任务,就将它添加到微任务队列中;
- 如果是宏任务,【如果是定时,或者需要手动触发的事件,则需要先加入event table中,等触发条件的时候才会加入到宏任务队列】就将其加入到宏任务队列中
- 执行完该宏任务下所有的同步任务之后,立即依次执行当前微任务队列中的所有微任务(清空微任务队列)。
- 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染。
- 渲染完毕后,JS线程继续接管,一个事件循环结束,开始下一个宏任务(从宏任务队列中获取),进入下一个事件循环,直到宏任务队列被清空。
示例
接下来通过两段代码体会一下事件循环机制的执行流程吧。
一、宏任务中包含微任务
<input id = "btn" type="button" value="点我啊">
<script>
setTimeout(() => {
console.log('timeout callback1'); //6
Promise.resolve(1).then(
value => {
console.log('成功了', value); //8
}
)
console.log('timeout callback11'); //7
}, 3000);
setTimeout(() => {
console.log('timeout callback2'); //4
}, 0) //最短时间为4ms
Promise.resolve(1).then
(
value => {
console.log('Promise onResolve', value); //2
}
)
Promise.resolve(2).then
(
value => {
console.log('Promise onResolve', value); //3
}
)
function fn1() {
console.log('fun1'); //1
}
fn1()
document.getElementById('btn').onclick = function () {
console.log('执行了btn'); //5
}
</script>
执行结果:
结果分析:
-
事件循环从宏任务 (macrotask) 队列开始,最初始,宏任务队列中,只有一个 script(整体代码)任务
-
遇到 setTimeout ①,setTimeout ② 依次加入到event table中【4ms 后②加入宏任务队列,3000ms后①加入宏任务队列中】
-
遇到 promise1 promise2 加入微任务队列中
-
遇到同步任务fn(),加入执行栈中执行
-
点击事件加入到event table中【当点击时才会触发事件加入到宏任务队列中】,我在3000ms内点击按钮,该任务③加入到宏任务队列中
-
同步任务执行完毕
-
依次执行微任务队列的任务(promise1 promise2 )
-
当前宏任务执行完,[重新渲染页面]
-
三秒后,宏任务队列中依次为②③①
-
取出宏任务队列中的setTimeout ②,
- 执行同步任务,打印”timeout callback1“
- 将promise加入微任务队列中
- 执行同步任务,打印”timeout callback11“,
- 执行微任务队列的任务(一个),打印”成功了 1“
-
依次取出③,①,分别循环执行1-9
二、微任务中包含宏任务
<script>
console.log('script start');
setTimeout(function() {
console.log('timeout1');
}, 10);
new Promise(resolve => {
console.log('promise1');
resolve();
setTimeout(() => console.log('timeout2'), 9);
}).then(function() {
console.log('then1')
})
console.log('script end');
</script>
运行结果:
结果分析:
- 事件循环从宏任务 (macrotask) 队列开始,最初始,宏任务队列中,只有一个 script(整体代码)任务
- 执行同步任务:打印”script start“
- setTimeout①加入event table中,10ms后加入宏任务队列中
- new promise 中的代码立即执行,输出”promise1“, 然后执行 resolve ,遇到 setTimeout②,加入event table中,9ms后加入宏任务列表中
- then ()中的回调函数加入到微任务队列中
- 执行同步任务:打印”script end“
- 依次执行微任务队列的任务,清空队列
- 宏任务队列中依次为 setTimeout②、setTimeout①,取出②,循环1-7
- 取出①,循环1-7
async、await
ES7引入了async和await 这样使得promise的调用更加简单和方便 同时他们与这个事件循环机制也有很大的关系
我们知道隐式返回 Promise 作为结果的函数,那么可以简单理解为,await后面的函数执行完毕时,把await后面的代码注册到微任务队列当中,然后直接跳出async函数,执行其他代码
示例
<script>
console.log('script start')
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 start')
await 7;
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>
执行结果:
结果分析:【注意标黄位置】
-
事件循环从宏任务 (macro task) 队列开始,最初始,宏任务队列中,只有一个 script(整体代码)任务
-
执行同步代码:打印”script start“
-
执行async1()函数,执行await后的 async2()函数
-
执行async2() 中的同步任务,打印”async2 start“,接着执行await后的7,
await 7;
执行完,将其后面的内容(记作a1)注册到微任务队列中。(注意:此时async2() 并没有执行完,因此async1()中的语句==await async2();
同样也没有执行完,因此await async2();
==后的内容并没有加入到微任务队列中!!!) -
继续向下执行,将setTimeout加入event table中,时间一到将回调函数加入宏任务队列
-
new promise 中的代码立即执行,输出”Promise“
-
将两个then中的回调函数(记作p1,p2)依次加入微任务队列中,此时微任务队列有 a1、p1、p2
-
执行同步代码:打印”script end”
-
清空微任务队列:
- 执行微任务a1,打印”async2 end “,此时async2()执行完毕,即async1()中的
await async2();
执行完毕,此时将await async2();
后的内容(记作a2)加入到微任务队列中,此时微任务队列有 p1、p2、a2 - 同上,依次执行 p1、p2、a2
- 执行微任务a1,打印”async2 end “,此时async2()执行完毕,即async1()中的
-
微任务队列已经清空,执行宏任务队列中的setTimeout中的回调函数,打印“setTimeout”