看完这篇事件循环eventloop,以后都不用看其他文章了

969 阅读5分钟

事件循环

文章没有废话,可以仔细看,慢慢看

浏览器的进程模型

浏览器是一个多进程,多线程的应用程序。

现代浏览器的内部工作极其复杂,为了避免相互影响,减少连环崩溃的几率,浏览器启动后,会启动多个进程

浏览器主要进程:浏览器进程,网络进程,渲染进程

浏览器进程:主要负责界面展示,用户交互,子进程管理。浏览器进程内部会启动多个线程处理不同任务

网络进程:负责加载网络资源。网络线程会启动多个线程来处理不同任务

渲染进程:渲染进程启动后,会开启一个渲染主线程(事件循环发生的位置),主线程负责执行 HTML , CSS , JS 代码,默认情况下浏览器会为每个标签页开启一个新的渲染进程,以保证每个标签页不相互影响

谷歌浏览器右上角 —> 更多工具 —> 任务管理器, 可以看到浏览器开启的进程

image.png

渲染主线程是如何工作的

渲染主线程是浏览器最繁忙的线程

  • 解析HTML
  • 解析CSS
  • 计算样式
  • 布局
  • 处理图层
  • 每秒把页面画60次
  • 执行全局JS代码
  • ......

要处理这么多的任务,浏览器如何调度任务呢?

  • 正在执行一个JS函数,执行到一半的时候,用户点击了按钮,立即去处理点击事件的处理函数吗?
  • 正在执行一个JS函数,执行到一半的时候,某个计时器到了时间,立即去执行计时器的回调吗?
  • 浏览器进程通知我,“用户点击了按钮” 与此同时某个计时器也到了时间,应该去执行哪一个呢?

渲染主线程处理方式:排队

image.png
  1. 浏览器开始时,渲染主线程会进入一个无限循环 for (;;){}

  2. 每次循环会检查消息队列中是否有任务存在,如果有,就取出第一个任务执行,执行完一个任务后进入下一次循环,如果没有,则进入休眠状态

  3. 其他的所有线程,包括其他进程的线程,可以随时向消息队列添加任务,新任务会被添加到消息队列的末尾,如果主线程是休眠状态,则会唤醒主线程

整个过程,w3c称之为事件循环 event loop(谷歌称之为 消息循环,message loop)

异步

代码在执行的过程中,会遇到一些无法立即执行的任务,比如:

  • 计时器带来的任务:setTimeout setInterval
  • 网络通信完成后需要执行的任务:xhr,http,axios
  • 用户操作后需要执行的任务: addEventListener

如果让渲染主线程等待这些任务的执行时机到来,就会导致主线程的阻塞吗,卡死

渲染主线程无论如何都不能阻塞,因此浏览器选择异步来处理这类问题

面试题:如何理解 JS 的异步?

JS 是一门单线程的语言,这是因为它运行在浏览器的渲染主线程中,渲染主线程只有一个。

渲染主线程承担着诸多工作,渲染页面,执行 JS 都包含其中,如果使用同步的方式,极有可能导致主线程产生阻塞,从而导致消息队列中的其他任务无法得到执行,这样一方面会导致本该繁忙的主线程在白白消耗时间等待,另一方面会阻碍页面无法及时更新,造成浏览器卡死现象。

所以浏览器选择异步的方式来避免。具体做法是,当遇到不能立即执行的任务时,比如:计时器,网络请求,事件监听,主线程将任务交给其他线程去处理,主线程立即结束任务的执行,转而执行后续的代码,当其他线程完成时,将事先传递的回调函数包装成任务,加入到消息队列的末尾排队,等待主线程调度执行

在这种异步模式下,浏览器永不阻塞,保证了单线程的流程运行

JS 为何会阻碍渲染?

    <h1>我很帅</h1>
    <button>按钮</button>
    
    <script>
        const h1 = document.querySelector('h1')
        const btn = document.querySelector('button')

        // 死循环指定时间
        function delay(duration){
            const time = Date.now()
            while(Date.now()-time<duration){}
        }

        btn.addEventListener('click',e=>{
            h1.textContent = '我更帅了'
            delay(3000)
        })
    </script>

上述代码,点击之后会立刻变帅吗? 答案是: 不会,会延时3秒后变帅

  1. const h1 = document.querySelector('h1') const btn = document.querySelector('button')
  2. function delay(duration){ const time = Date.now() while(Date.now()-time<duration){} }
  3. btn.addEventListener('click',fn) 主线程告诉交互线程,这里有个监听事件交给你,我先忙去了
  4. 当用户点击之后,监听线程会把回调函数包装成任务加入消息队列排队
  5. 主线程拿到任务 执行 fn
  6. h1.textContent = '我更帅了' 产生绘制任务,交给消息队列排队
  7. delay(3000) 死循环3000ms
  8. 主线程执行完,继续去消息队列取,拿到绘制任务,绘制页面

任务有优先级吗?

任务没有优先级,在消息队列中先进先出,但是消息队列是有优先级的

过去我们说,浏览器分为宏任务和微任务,

根据W3C的最新解释:

  • 每个任务都有一个任务类型,同一个类型的任务必须在同一个队列,不同类型的任务可以属于不同队列
  • 浏览器必须有一个微队列,微任务队列中的任务优先于所有其他队列中的任务 html.spec.whatwg.org/multipage/w…

在目前的 chrome 的实现中,至少包含了下面的队列:

  • 延时队列:用于存放计时器到达后产生的回调任务,优先级中
  • 交互队列:用于存放用户操作后产生的事件处理,优先级高
  • 微队列:用于存放需要最快执行的任务,优先级最高

添加任务到微队列的主要方式是

Promise.resolve().then(函数)

MutationObserver

image.png

渲染主线程,优先从微队列取出任务执行,其次交互队列,最后延时队列。

面试题:阐述一下JS的事件循环

事件循环又叫做消息循环,是浏览器渲染主线程的工作方式

在chrome中,chrome会开启一个 不会结束的for循环,每次循环从消息队列中取出第一个任务进行执行,而其他线程只需要在合适的时候将任务加入到对应的队列的末尾即可。

过去把消息队列分为宏任务队列和微任务队列,目前已无法满足浏览器复杂的环境

根据W3C最新解释,每个任务有自己的任务类型,同类型的任务必须在同一个队列,不同的任务可以属于不同的队列。不同的任务队列有不同的优先级,在一次事件循环中,由浏览器决定取哪一个队列的任务,但是浏览器必须有一个微队列,微队列具有最高的优先级

写文章不易,看官多多点个赞。