道招

《浏览器工作原理与实践》笔记之事件循环队列

如果您发现本文排版有问题,可以先点击下面的链接切换至老版进行查看!!!

《浏览器工作原理与实践》笔记之事件循环队列

为了能让你更加深刻地理解事件循环机制,我们就从最简单的场景来分析,然后带你一步步了解浏览器页面主线程是如何运作的。

使用单线程处理安排好的任务

我们先从最简单的场景讲起,比如有如下一系列的任务:

任务 1:1+2 任务 2:20/5 任务 3:7*8 任务 4:打印出任务 1、任务 2、任务 3 的运算结果

现在要在一个线程中去执行这些任务,通常我们会这样编写代码:


void MainThread(){
     int num1 = 1+2; //任务1
     int num2 = 20/5; //任务2
     int num3 = 7*8; //任务3
     print("最终计算的值为:%d,%d,%d",num1,num2,num3); //任务4
  }

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

可以参考下图来直观地理解下其执行过程:

在线程运行过程中处理新任务

但并不是所有的任务都是在执行之前统一安排好的,大部分情况下,新的任务是在线程运行过程中产生的。为了能接收并执行新的任务,就需要采用事件循环机制。我们可以通过一个 for 循环语句来监听是否有新的任务。

如下面的示例代码:


//GetInput
//等待用户从键盘输入一个数字,并返回该输入的数字
int GetInput(){
    int input_number = 0;
    cout<<"请输入一个数:";
    cin>>input_number;
    return input_number;
}

//主线程(Main Thread)
void MainThread(){
     for(;;){
          int first_num = GetInput();
          int second_num = GetInput();
          result_num = first_num + second_num;
          print("最终计算的值为:%d",result_num);
      }
}

相较于第一版的线程,这一版的线程做了两点改进。

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

改进版的进程如下

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

上面我们改进了线程的执行方式,引入了事件循环机制,可以让其在执行过程中接受新的任务。不过在第二版的线程模型中,所有的任务都是来自于线程内部的,如果另外一个线程想让主线程执行一个任务,利用第二版的线程模型是无法做到的。那下面我们就来看看其他线程是如何发送消息给渲染主线程的,具体形式你可以参考下图:

从上图可以看出,渲染主线程会频繁接收到来自于 IO 线程的一些任务,接收到这些任务之后,渲染进程就需要着手处理,比如接收到资源加载完成的消息后,渲染进程就要着手进行 DOM 解析了;接收到鼠标点击的消息后,渲染主线程就要开始执行相应的 JavaScript 脚本来处理该点击事件。

有了队列之后,我们就可以继续改造线程模型了,改造方案如下图所示:

浏览器的渲染进程专门有一个 IO 线程用来接收其他进程传进来的消息,接收到消息之后,会将这些消息组装成任务发送给渲染主线程,后续的步骤就和前面讲解的“处理其他线程发送的任务”一样了,这里就不再重复了。

消息队列中的任务类型有很多内部消息类型,如输入事件(鼠标滚动、点击、移动)、微任务、文件读写、WebSocket、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 变化非常频繁,如果每次发生变化的时候,都直接调用相应的 JavaScript 接口,那么这个当前的任务执行时间会被拉长,从而导致执行效率的下降。

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

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

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

针对这种情况,微任务就应用而生了,下面我们来看看微任务是如何权衡效率和实时性的。

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

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

问题二、第二个是如何解决单个任务执行时长过久的问题。

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

从图中你可以看到,如果在执行动画过程中,其中有个 JavaScript 任务因执行时间过久,占用了动画单帧的时间,这样会给用户制造了卡顿的感觉,这当然是极不好的用户体验。针对这种情况,JavaScript 可以通过回调功能来规避这种问题,也就是让要执行的 JavaScript 任务滞后执行。至于浏览器是如何实现回调功能的,我们在后面的章节中再详细介绍。

更新时间:
上一篇:《浏览器工作原理与实践》笔记之垃圾回收下一篇:CKEditor4是怎么跑起来的(一)

相关文章

财猫省钱浏览器使用心得

前天好像是在华军软件园下载个什么东西(忘记了),看到了财猫省钱浏览器,今天就下载下来看看,发现正如它自己说的,它可以实现购物返利,变相省钱了,于是我用财猫省钱浏览器看看了几大购物网站。截图如下: 1 阅读更多…

财猫省钱浏览器针对java异常hold住

最近想自学java的,准备用网页调用applet,可能是由于java哪里的设置有点毛病,总是提示有问题,然后IE浏览器就假死了,只好用任务管理器把它关掉,郁闷啊。最后我想到了最近刚安装的财猫省钱浏览器 阅读更多…

一行代码,轻松将浏览器变成临时编辑器

这是 Jose 在CoderWall 分享的一个 小技巧 :在浏览器地址栏中输入一行代码: [code lang="php"]data:text/html, &amp;lt;html content 阅读更多…

工行,你真的不管firefox了吗

今天换了个小号浏览器(大号是chrome,ie已卸载)还上网看东东,无意中想登下工行的官方网站,发现了雷人的一面。 算了,工行,你直接说不支持firefox,我还好受点,总不能让我们停留在古代 阅读更多…

关注道招网公众帐号
道招开发者二群