zoukankan      html  css  js  c++  java
  • 浅析事件循环(Event Loop)

    说到事件循环就不可避免的会谈到到任务队列,宏任务,微任务等等这些名词。那么问题来了,设计事件循环系统是为了解决什么问题,有了宏任务为什么还要有微任务??

    单线程的JavaScript和多进程的浏览器

    ​ JavaScript这个语言在设计之初就是单线程,原因当然不是当初多核CPU还不够普及。作为主战场在浏览器的脚本语言,JavaScript的主要用途是与用户交互相关,以及操作DOM。这个场景决定了单线程比多线程更合适。比如同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程的操作结果为准?

    ​ 随着多核CPU的普及以及JavaScript的应用场景的转换,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以新标准并没有改变JavaScript单线程的本质。

    ​ 虽然JavaScript是单线程的,但是具有复杂功能的现代浏览器却是多线程的。

    ​ Chrome浏览器包括:1个浏览器(Browser)主进程、1个 GPU 进程、1个网络(NetWork)进程、多个渲染进程和多个插件进程

    • 主进程: 界面显示、用户交互、子进程管理,同时提供存储等功能
    • 插件进程:每个启动的插件都会创建一个进程
    • GPU进程:最初GPU是为了实现3D效果,后来Chrome选择采用GPU来绘制UI界面
    • 网络进程:主要负责页面的网络资源加载,之前是作为一个模块运行在浏览器进程里面的,直至最近才独立出来,成为一个单独的进程。
    • 渲染进程:也是所谓的浏览器内核,核心任务是将 HTML、CSS 和 JavaScript 转换为用户可以与之交互的网页,排版引擎Blink和JavaScript引擎V8都是运行在该进程中,默认情况下,Chrome会为每个Tab标签创建一个渲染进程。
    任务队列和事件循环

    ​ 首先JavaScript的使用场景决定了它是单线程的。现在假定JavaScript引擎在执行script代码时,用户点击了页面上某个按钮,如果这个时候JavaScript引擎还在执行代码,那么只能由浏览器起一个线程来记录这个点击,等JavaScript引擎忙完了再来告诉它某个时候鼠标在某个坐标点击了一个按钮,需要你去执行按钮注册的回调函数。

    ​ 再进一步升级,如果有期间有多个事件发生了,那么需要一个队列来存储所有的事件,按照先来后到的顺序依次被执行。为了让这个事情更加的高效,事件循环系统就应运而生了,JavaScript引擎不断的从调用栈中取出任务执行,事件循环系统一次次的检查调用栈是否为空。如果调用栈为空,那么就会从任务队列中取出最早的一个任务来加入到调用栈中。

    消息队列.png

    宏任务和微任务

    上面提到的任务队列就是宏任务队列,里面都是宏任务。可能看起来宏任务和事件循环已经可以满足需求了,那为什么还要有微任务和微任务队列。首先看看哪些任务属于宏任务哪些又属于微任务

    • 宏任务(macrotask)
      • 渲染事件(如解析 DOM、计算布局、绘制)
      • JavaScript 脚本执行事件
      • 用户交互事件(如鼠标点击、滚动页面、放大缩小等)
      • 网络请求完成(XMLHttpRequest)、文件读写完成(I/O)
      • 定时器任务(setTimeout,setInterval)
    • 微任务(microtask)
      • MutationObserver
      • Promise

    宏任务可以满足大多数对时效要求不高的需求。一个最简单的例子setTimeout回调函数的执行时机问题,定时器触发线程只能保证在指定间隔时间之后,将回调函数加入到宏任务队列的队尾。因为每轮事件循环只有一个宏任务被执行,如果队列中已经有了很多任务,那么必定会影响后添加的宏任务的执行时机。所以类似setTimeout这些宏任务时间粒度比较粗,并不能精准的控制执行时机。

    主流浏览器中目前还在用的产生微任务的两种方法。1,使用MutationObserver来观察DOM变化,当被监控的DOM属性,子节点,文本发生变化时,指定的回调函数便作为一个微任务添加到微任务队列。2,Promise.resolve()和Promise.reject()时,Promise的状态由pending变化为resolved或者rejectd之后,相应的回调函数也作为一个微任务添加到微任务队列。

    一轮完整的事件循环

     <div>
     	<button id="firstBtn" style="background-color: green;">第一个按钮</button>
     	<button id="secondBtn">第二个按钮</button>
     </div>
    <script>
        console.log("script start");
    
        // 第一个按钮监听点击
        document.querySelector("#firstBtn").addEventListener('click',()=>{
            console.log("firstBtn click");
            setTimeout(()=>{
                console.log("setTimeout1");
            },100)
            Promise.resolve().then(()=>{
                console.log('Promise3 callback');
            })
        })
    
        // 第二个按钮监听点击
        document.querySelector("#secondBtn").addEventListener('click',()=>{
            var el = document.querySelector("#secondBtn")
            console.log("secondBtn click");
            Promise.resolve().then(()=>{
                console.log('Promise4 callback');
            })
    
            setTimeout(()=>{
                console.log("setTimeout2");
            },0)
            for(let i=0;i<100;i++){
                el.style.color = i % 2 == 0 ? "blue" : "red";
            }
        })
    
        // 创建MutationObserver实例,监听第二个按钮DOM变化
        var observer = new MutationObserver((mutations,observer)=>{
            console.log("mutations callback");
        })
    
        observer.observe(document.querySelector("#secondBtn"),{
            childList:true,
            attributes:true,
        })
    
        console.log("script end");
    
    </script>
    

    1,在JavaScript引擎解析DOM并绘制页面之后,执行script脚本

    2,控制台打印script start script end

    3,如果点击了按钮1,按钮1的click回调函数添加到宏任务队列

    4,调用栈为空,将回调函数从宏任务队列取出到调用栈中执行

    5,控制台打印firstBtn click,Promise回调函数添加到微任务队列,定时器回调添加到宏任务队列

    6,调用栈为空,依次执行微任务,直至清空微任务队列,控制台打印Promise3 callback,然后浏览器可以选择是否重新渲染页面

    7,定时器触发线程经过100ms后触发,根据id将对应的定时器回调函数添加到宏任务队列中

    8,调用栈为空,将定时器回调函数从宏任务队列取到调用栈中执行,控制台打印setTimeout1,宏任务和微任务队列都为空。

    Snipaste_2020-04-26_16-35-46.png

    9,如果此时点击了按钮2,同样会将按钮2点击事件回调函数加入到宏任务队列

    10,控制台打印secondBtn click,Promise回调添加到微任务队列,定时器回调添加到宏任务队列,然后执行for循环中的DOM修改操作

    11,100次for循环执行完,调用栈为空,MutationObserver因为监听到DOM修改,回调函数作为微任务添加到微任务队列

    12,控制台打印Promise4 callbackmutations callback(仅打印一次),setTimeout2

    这个打印顺序(mutations callback 在 setTimeout2之前)也验证了MutationObserver回调函数是个微任务。而且可以注意到for循环100次中每次都修改了DOM属性,但是MutationObserver仅触发了一次,也验证了MutationObserver回调函数是异步执行的,最后只统一执行一次,这也是MutationObserver 比MutationEvent性能更佳的原因

    如果想让mutations callback打印100次该怎么做呢?

    for(let i=0;i<100;i++){
        setTimeout(function(){
            el.style.color = i % 2 == 0 ? "blue" : "red";
        },0)
    }
    

    参考文档

    JavaScript忍者秘籍(第2版)

    浏览器工作原理

    In The Loop Jake Archibald@JSconf 2018

    Philip Roberts- What the heck is the event loop anyway? | JSConf EU 2014

  • 相关阅读:
    Linux环境下使用eclipse开发C++动态链接库程序
    例解 autoconf 和 automake 生成 Makefile 文件
    linux下编译boost
    在linux下如何编译C++程序
    windows和linux套接字中的select机制浅析
    看到关于socket非阻塞模式设置方式记录一下。
    MySQL批量执行sql文件
    Sqlcmd使用详解
    批量执行SQL文件
    SpringCloud微服务之跨服务调用后端接口
  • 原文地址:https://www.cnblogs.com/madlife/p/12782468.html
Copyright © 2011-2022 走看看