浏览器的事件循环及渲染

211 阅读2分钟

这篇文章会通过几个问题及示例来说明事件循环

一、什么是事件循环

浏览器负责执行脚本,渲染页面,响应用户操作。为了保证多项任务不会冲突,浏览器有一个主线程负责执行代码,其他线程去处理一些异步操作。等到异步操作结束,会放入任务队列中,主线程从任务队列中取下一个要执行的任务。这个是事件循环中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会在浏览器渲染之前调用,也会一直执行直到队列清空,如果动画内部有回调,会在下一帧执行