美文网首页浏览器专栏学习记录
浏览器专栏学习-消息队列和事件循环

浏览器专栏学习-消息队列和事件循环

作者: Mstian | 来源:发表于2020-08-22 17:20 被阅读0次

    此学习记录来自于极客时间专栏浏览器工作原理与实践,由于个人对这块内容比较感兴趣,所以花钱买了专栏,但看完总觉得什么都没记住,所以将一些重要内容记录下来。文中有老师课程中的原文和配图,已在课程留言区告知老师,如侵删。
    如果对这块内容感兴趣,强烈建议去读一遍老师的课程,很有收获。

    消息队列和事件循环

    每个渲染进程都有一个主线程,并且主线程非常繁忙,既要处理DOM,又要计算样式,还要处理布局,同时还需要处理JavaScript任务以及各种输入事件,要让这么多不同类型的任务在主线程中有条不紊地执行,这就需要一个系统来统筹调度这些任务,这个统筹调度任务就是消息队列和事件循环系统

    第一版线程:使用单线程处理安排好的任务

    假如有四个任务:

    • 任务一:1+2
    • 任务二:10-2
    • 任务三:9*8
    • 任务四:打印输出任务1234.

    现在在一个线程中执行这些任务:

    function MainThread(){ // 主线程
        let num1 = 1+2;
        let num2 = 10-2;
        let num3 = 9*8;
        console.log(num1,num2,num3);
    }
    

    在上面代码执行过程中,所有任务代码按照顺序写进主线程里,等线程执行时,这些任务会按照顺序在线程中依次被执行,等所有任务执行完成之后,线程会自动退出。

    线程的一次执行

    第二版线程:在线程运行过程中处理新任务

    任务并不是一开始都安排好去等待线程处理的,比如在线程运行的过程中会有新的任务不断产生的时候,第一版线程就无法去处理。
    要想在线程运行过程中,能接收并执行新的任务,就需要采用事件循环机制

    // 伪代码
    function GetInput(){
        let num = prompt("请输入数字");
        if(typeof Number(num) === 'number'){
            return num;
        }
    }
    
    function MainThread(){ // 主线程
        for(; ;){
            let num1 = GetInput();
            let num2 = GetInput();
            return num1+num2;
        }
    }
    

    相比较第一版的线程这一版有两点改动:

    • 第一点引入循环机制,具体表现在在线程语句最后添加了一个for循环语句,线程会一直循环执行。
    • 第二点引入了事件,可以在线程运行过程中,等待用户输入数字,等待过程中线程处于暂停状态,一旦接收到用户输入信息,那么线程会被激活,然后执行相加运算,最后输出结果。

    如下图所示:


    在线程中引入事件循环

    第三版线程:处理其他线程发送过来的任务

    第二版线程引入事件循环机制,可以让其在执行过程中接收新的任务。不过所有的任务都是来自于线程内部,如果另外一个线程想让主线程执行一个任务,利用第二版的线程模型是无法做到的。

    其他线程如何发送消息给主线程呢???

    比如IO线程会频繁给主线程发送鼠标点击事件,资源加载完成事件等等其他事件。

    渲染进程之间发送任务

    比如接收到资源加载完成的消息后,渲染进程就要着手进行DOM解析了;接收到鼠标点击的消息后,渲染进程就要开始执行相应的JavaScript脚本来处理该点击事件。

    那么如何设计好一个线程模型,能让其能够接收其他线程发送的消息呢?

    一个通用的模式是消息队列

    老师的图画的太好了,真心推荐老师的课程

    消息队列是一种数据结构,可以存放要执行的任务。它符合“先进先出”的特点,也就是说添加任务是在队列的尾部,取出任务,是在队列的头部。

    还是看图:


    线程模型 队列+循环

    现在线程执行任务步骤大致为

    1. 添加一个消息队列;
    2. IO线程中产生的新任务添加进消息队列尾部;
    3. 渲染主线程会循环地从消息队列头部读取任务,执行任务。

    处理其他进程发送过来的任务

    通过使用消息队列,实现了线程之间的消息通信,在Chrome中,跨进程之间的任务也是频繁发生的,那么如何处理其他进程发送过来的任务?

    跨进程发送消息

    渲染进程专门有一个IO线程用来接收其他进程传进来的消息。接收到消息之后,会将这些消息组装成任务发送给渲染主线程,后续的步骤跟处理其他进程发送的任务一样了。

    小结:

    • 如果有一些确定好的任务,可以使用一个单线程来按照顺序处理这些任务,这是第一版线程模型。
    • 要在线程执行过程中接收并处理新的任务,就需要引入循环语句事件系统,这是第二版线程模型。
    • 如果要接收其他线程发送过来的任务,就需要引入消息队列,这是第三版线程模型。
    • 如果其他进程想要发送任务给渲染主线程,那么先通过IPC把任务发送给渲染进程的IO线程。IO线程再把任务发送给渲染主线程。

    以上就是线程在执行任务的一系列情况。

    消息队列中的任务类型

    现在大概了解了页面主线程(渲染主线程)是如何接收外部任务了,那接下来看看消息队列中的任务类型有哪些,包含输入事件(鼠标滚动、点击、移动)、微任务、文件读写、WebSoket、JavaScript定时器等。

    除此之外,消息队列还包含了很多与页面相关的事件,如JavaScript执行、解析DOM、样式计算、布局计算、CSS动画等。

    以上这些事件都是在主线程中执行的。

    如何安全退出

    当页面主线程执行完成之后,又该如何保证页面主线程能够安全退出呢?Chrome是这样解决的,确定要退出当前页面时,页面主线程会设置一个退出标志的变量,在每次执行完一个任务时,判断是否有设置退出标志。

    如果设置了,那么就直接中断当前所有任务,退出线程。

    TaskQueue task_queue;
    void ProcessTask();
    bool keep_running = true;
    void MainThread(){
      for(;;){
        Task task = task_queue.takeTask();
        ProcessTask(task);
        if(!keep_running) //如果设置了退出标志,那么直接退出线程循环
            break; 
      }
    }
    

    页面使用单线程的缺点

    以上的所有情况我们都没有说异步的概念,所有任务都是先进先出,从头到尾一个接着一个执行的,但是这样会存在一些问题。
    页面线程所有执行的任务都来自消息队列,消息队列是先进先出的属性,也就是说放入消息队列任务需要等待前面的任务被执行完,才会被执行。这样就会存在两个问题。

    第一个问题是如何处理高优先级的任务
    比如DOM节点变化时根据这些变化来处理响应的逻辑,一个通用的设计是利用JavaScript设计一套监听接口,当变化发生时,渲染引擎同步调用这些接口,这是一个典型的观察者模式。

    不过这个模式有问题,因为DOM变化非常频繁,如果每次发生变化,都直接响应,那么当前的任务执行时间就会被拉长,从而导致执行效率下降。

    如果将这些DOM变化做成异步的消息事件,添加到消息队列的尾部,那么又会影响到监控的实时性,因为在添加到消息队列的过程中,可能前面就有很多任务在排队了。

    这也就是说,如果DOM发生变化,采用同步通知的方式,会影响当前任务的执行效率,如果采用异步方式,又会影响到监控的实时性

    那该如何权衡效率实时性呢?

    针对这种情况,微任务就应运而生了

    通常我们把消息队列中的任务称为宏任务,每个宏任务中都包含了一个微任务队列,在执行宏任务的过程中,如果DOM有变化,那么就会将该变化添加到微任务列表中,这样就不会影响到宏任务的继续执行,因此解决了执行效率的问题

    等宏任务中的主要功能都直接完成之后,这时候渲染引擎并不着急去执行下一个宏任务,而是执行当前宏任务中的微任务,因为DOM变化的事件都保存在这些微任务队列中,这样也就解决了实时性问题

    建议:在看这块内容时候,将前面的图记在脑子里,如果忘了那就边看图,边看这块内容,效果会更好。

    第二个问题是如何解决单个任务执行时间过长的问题
    因为所有的任务都是在单线程中执行的,所以每次只能执行一个任务,而其他任务都需要处于等待状态,如果其中一个任务执行时间过久,那么下一个任务就需要等待很长时间,可以参考下图:

    单个任务执行时间过长

    从图中看出,如果在执行动画过程中,有个js任务因执行时间过久,占用了动画单帧的时间,这样会给用户造成卡顿的感觉,这当然是极不好的用户体验,针对这种情况,JacaScript可以通过回调来规避这种问题,也就是让要执行的js任务滞后执行。

    tips:

    1. 因为JavaScript引擎是运行在渲染进程的主线程上的,所以我们说JavaScript是单线程的。
    2. JavaScript单线程指的是浏览器中负责解释和执行JavaScript代码的只有一个线程,JavaScript引擎线程,除此之外,浏览器还有其他四个线程事件触发线程定时器触发线程http请求线程GUI渲染线程。当遇到计时器、DOM事件监听或是网络请求时,JS引擎会将其交给webapi,也就是浏览器提供的相应线程。而JS引擎则继续后边的其他任务,以此方式实现异步非阻塞。当计时器,DOM事件监听等完成后,他会将响应的回调函数交给消息队列,等待消息队列中前面任务执行完成之后需再执行回调函数里的任务。

    推荐文章:JavaScript中的事件循环&消息队列

    相关文章

      网友评论

        本文标题:浏览器专栏学习-消息队列和事件循环

        本文链接:https://www.haomeiwen.com/subject/mdwvjktx.html