《浏览器工作原理与实践》笔记之事件循环队列
《浏览器工作原理与实践》笔记之事件循环队列
为了能让你更加深刻地理解事件循环机制,我们就从最简单的场景来分析,然后带你一步步了解浏览器页面主线程是如何运作的。
使用单线程处理安排好的任务
我们先从最简单的场景讲起,比如有如下一系列的任务:
任务 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 任务滞后执行。至于浏览器是如何实现回调功能的,我们在后面的章节中再详细介绍。
- 分类:
- Web前端
相关文章
一行代码,轻松将浏览器变成临时编辑器
这是 Jose 在CoderWall 分享的一个 小技巧 :在浏览器地址栏中输入一行代码: [code lang="php"]data:text/html, &lt;html content 阅读更多…
工行,你真的不管firefox了吗
今天换了个小号浏览器(大号是chrome,ie已卸载)还上网看东东,无意中想登下工行的官方网站,发现了雷人的一面。 算了,工行,你直接说不支持firefox,我还好受点,总不能让我们停留在古代 阅读更多…
财猫省钱浏览器针对java异常hold住
最近想自学java的,准备用网页调用applet,可能是由于java哪里的设置有点毛病,总是提示有问题,然后IE浏览器就假死了,只好用任务管理器把它关掉,郁闷啊。最后我想到了最近刚安装的财猫省钱浏览器 阅读更多…
财猫省钱浏览器使用心得
前天好像是在华军软件园下载个什么东西(忘记了),看到了财猫省钱浏览器,今天就下载下来看看,发现正如它自己说的,它可以实现购物返利,变相省钱了,于是我用财猫省钱浏览器看看了几大购物网站。截图如下: 1 阅读更多…