这篇文章会通过几个问题及示例来说明事件循环
一、什么是事件循环
浏览器负责执行脚本,渲染页面,响应用户操作。为了保证多项任务不会冲突,浏览器有一个主线程负责执行代码,其他线程去处理一些异步操作。等到异步操作结束,会放入任务队列中,主线程从任务队列中取下一个要执行的任务。这个是事件循环中js的一部分; 在一个事件循环中,不止会执行任务,还会进行页面渲染。当渲染完成后,进入下一个循环。所以一个完整的事件循环包括执行JS和渲染两部分,这两部分可以细分为宏任务-微任务-RAF回调任务-布局渲染四个步骤
栗子一:一次事件循环中包含JS和渲染两部分,所以异步代码不会阻塞页面渲染
// index.js
setInterval(() => {
console.log('111');
});
// index.html
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="./index.js"></script>
</head>
<body>
<div id="app">
<div>demo</div>
</div>
</body>
</html>
虽然setInterval一直在运行,但不会影响到页面的交互,因为每一次setInterval都是一次事件循环。但如果把setInterval变为同步代码,那页面就会无法渲染和交互
// index.js
while(true){
}
栗子二:当前js代码执行完成后,才会渲染
const appEle = document.getElementById('app');
for(let i = 0;i < 10;i ++) {
const ele = document.createElement('span');
ele.innerText = 'I am span';
appEle.appendChild(ele)
}
setTimeout(() => {
const appEle = document.getElementById('app');
appEle.innerHTML = null;
}, 1000);
span元素不是一个一个添加上的,而是一起显示到页面上的。并且页面会先显示span元素,之后消失,因为第一次事件循环中添加了span元素,第二次删除了元素
二、宏任务和微任务
在浏览器执行完当前任务,进行页面渲染之前,我们想做一些其他操作。这个时候就需要微任务。微任务会在当前js调用栈的任务完成后,渲染之前执行;如果微任务没有按照预期执行,可以检查当前堆栈是否为空。
栗子一:微任务会阻塞页面渲染(在渲染之前执行)
new Promise((resolve) => {
resolve();
}).then(() => {
while(true){}
});
栗子二:微任务会在当前任务执行完,渲染之前执行
setTimeout(() => {
const appEle = document.getElementById('app');
appEle.innerHTML = 'change app inner text';
new Promise((resolve) => {
resolve();
}).then(() => {
appEle.innerHTML = 'then change app inner text';
});
}, 1000);
// html
<div id="app">
<div>demo</div>
</div>
上面页面的效果是,先显示demo,之后变为then change app inner text,而change app inner text并没有显示到页面上
三、RAF回调任务
除了宏任务和微任务,其实还有一类任务,requestAnimationFrame回调任务,根据MDN定义,requestAnimationFrame会在浏览器渲染之前调用,也会一直执行直到队列清空,如果动画内部有回调,会在下一帧执行