一文带你浅浅了解浏览器事件循环机制

Ccat2023/3/30FrontSide浏览器

一文带你浅浅了解浏览器事件循环机制

导题

前几天看到这样一个简单的例子,按照常理来分析的话结果应该是一点击按钮后h1内部的内容就发生改变。然而在chrome浏览器上运行这段代码时可以发现在3s以后h1内部的内容才发生改变,这是为啥呢????

<!DOCTYPE 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>
</head>
<body>
    <h1 id="text">Hello Ccat</h1>
    <button id="btn">点击切换</button>
    <script>
        const text = document.getElementById("text")
        const btn = document.getElementById("btn")

        function Stop(time){
            const now = Date.now()
            while(Date.now()-now < time);
        }

        btn.addEventListener("click",()=>{
            console.log(111);
            text.innerText = "你好 懵睡猫"
            Stop(3000)
        })
    </script>
</body>
</html>

刚刚点击按钮时: image.png

点击按钮3s后: image.png

浏览器渲染进程与渲染主线程

要搞懂这一点,最重要的是要弄懂浏览器的渲染进程是如何处理我们的js代码的。以谷歌浏览器为例,我们的浏览器分为浏览器主进程、网络进程、渲染进程等等。这里重点说一说渲染进程。

image.png

我们打开chrome的任务管理器可以看到,浏览器为我们的每一个页面都分配了一定的内存空间资源,也就代表着每一个标签页都是一个独立的渲染进程。渲染进程中的渲染主线程又在其中最为重要。

我们的渲染主线程主要为页面提供以下的功能:

  1. 解析HTML,CSS文件

  2. 样式的计算

  3. 布局

  4. js代码在浏览器上的执行

  5. 1s60次的页面重绘

    ......

事件队列及处理机制

这么一看,渲染主线程承担了许许多多的任务,那么他该如何处理任务,怎么样处理才更为准确高效成为一个很重要的问题。浏览器内部采取了这样的机制,渲染主线程一次性只能执行一个任务,而且这个任务一定要执行完,剩下的队列就采用队列的数据结构进行储存。(此处的任务可以表示渲染主线程的任何功能任务,下文可以简单理解为运行js代码段)

image.png

当渲染主线程的任务完成以后,将会从事件队列中的头部取出第一个任务继续执行。

image.png

这是个很容易理解的设计理念,但是当我们面对一些异步条件的时候这种机制是否还能保证效率呢?假设我们设置了5s的定时器,如果采取同步的方式那么效果应该是像下面这张图一样,带有定时器的任务在定时以后对主线程的运行造成了阻塞,显然是不符合我们对效率的要求的。

image.png

于是浏览器选择了异步的事件处理方式。实际上,计时器并不是由渲染主线程来计时的,渲染主线程执行到settimeout代码段后通知浏览器主线程中的计时线程来进行计时。由于只有渲染主线程能够执行js代码,所以当计时时间到达以后计时线程再将回调函数fun放入事件队列队尾。

image.png

看到现在,你可能会说开头那段代码顺序还是不能够解释啊?不,我们已经可以来解释开头那段代码了。不过还需要涉及多一个概念————js阻碍渲染。

const text = document.getElementById("text")
const btn = document.getElementById("btn")

function Stop(time){
    const now = Date.now()
    while(Date.now()-now < time);
}

btn.addEventListener("click",()=>{
    console.log(111);
    text.innerText = "你好 懵睡猫"
    Stop(3000)
})

当我们执行到text.innerText那一步时,浏览器内部实际上已经收到了我们要修改h1内容的信息,它也确实做了,不过距离我们能够在页面上看到它改变还差一步渲染。这个渲染也作为一个任务被加入到了事件队列的队尾。

代码执行到了Stop(3000),浏览器就会不断执行while循环(这不算一种阻塞,因为他在执行js代码),导致不能从事件队列中取出新的任务执行。

当Stop()函数执行完了以后,渲染主线程终于能够从队列中依次取出事件,直到取到渲染任务以后才终于将我们看到的h1里面的内容更改过来。整个页面重新渲染的过程由于js代码导致无法正常进行,这就是js阻碍渲染。

总结

还有一个问题,为什么叫事件循环呢?这是因为浏览器源码里面渲染主线程对于事件队列的检测是放在一个循环里面的,渲染主线程不停地对事件队列进行检测。

对于事件队列,其实浏览器内部对其进行了更为细致的划分,以及微任务的概念,大家如果有兴趣也可以进行了解,后续也会写文进行补充。

如有不正确的地方,欢迎指出。