阅读 91

浏览器中的页面循环系统

1. 消息队列和事件循环

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

接下来让我们从最简单的场景开始分析,一步一步了解浏览器页面主线程是如何运作的。

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

假如有一系列下列这些任务:

任务1:1+2 任务2:21 / 7 任务3:7*8 任务4:打印前3个任务的结果 复制代码

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

function MainThread() {     let num1 = 1+2     let num2 = 21/7     let num3 = 7*8     console.log(num1,num2,num3) } 复制代码

它的执行过程参考下图: 8-1.png

1.2 在线程执行过程中执行新任务

但并不是所有的任务都是在执行之前统一安排好的,大部分情况下,新的任务是在线程运行过程中产生的。比如在线程执行过程中,又接收到了一个新的任务要求计算“21+6”,那么该怎么做呢?

想在线程运行过程中,能接收并执行新的任务,就需要采用事件循环机制

我们可以用一个循环来监听是否有新的任务。

// 等待用户从键盘输入一个数字,并返回 function getInput() {     return prompt("请输入一个数字:") } // 主线程 (Main Thread) function MainThread() {     ... // 之前的任务     while(true) {         let firstNum = getInput()         let secondNum = getInput()         let sum = firstNum + secondNum         console.log("最终的计算结果为:"+sum)     } } 复制代码

相较于之前,这一版引入了循环机制,线程会一直循环执行。还引入了事件,可以运行在线程之中。等待过程线程处于暂停状态,一旦接收到新的任务,线程就会被激活,执行相应的任务。 8-2.png

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

上述我们考虑的新加的任务都是来自线程内部的,但如果遇到了外部线程发来的任务应该怎么处理呢?接下来进一步对上述模型进行改造。 8-3.png 渲染主线程会频繁接收到来自于 IO 线程的一些任务,接收到这些任务之后,渲染进程就需要着手处理,比如接收到资源加载完成的消息后,渲染进程就要着手进行DOM 解析;接收到鼠标点击的消息后,渲染主线程就要开始执行相应的 JavaScript 脚本来处理该点击事件。

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

一个通用模式是使用消息队列8-4.png 我们的改造可以分为下面三个步骤:

  1. 添加一个消息队列;

  2. IO 线程中产生的新任务添加进消息队列尾部;

  3. 渲染主线程会循环地从消息队列头部中读取任务,执行任务。

创建一个队列:

function getElement(arr, items) {   Array.from(arr).forEach((item) => {     if (item instanceof Array) {       getElement(item.reverse(), items)     } else {       items[items.length] = item     }   }) } function TaskQueue() {   // 基于数组实现   this.items = []   // 创建的时候传入可迭代结构怎么办?不能直接初始化嘛   //1.添加新的项   TaskQueue.prototype.enqueue = function() {     getElement(arguments, this.items.reverse())     this.items.reverse()   }   //2.移除操作,返回被移除的项   TaskQueue.prototype.dequeue = function() {     let temp = this.items[this.items.length - 1]     this.items.length--       return temp   }   // 3.返回队列的第一个元素   TaskQueue.prototype.front = function() {     return this.items[this.items.length - 1]   }   // 4.判断队列是否为空   TaskQueue.prototype.isEmpty = function() {     return this.items.length == 0   }   // 5.返回队列大小   TaskQueue.prototype.size = function() {     return this.items.length   }   // 6.将队列中的内容转换为字符串   TaskQueue.prototype.toString = function(punctuation) {     return this.items.join(punctuation)   } } 复制代码

改造一下主线程:

// 主线程 (Main Thread) function MainThread() {     ... // 之前的任务     while(true) {         let task = TaskQueue.dequeue()         ProcessTask(task)     } } 复制代码

如果有其他线程想要发送任务让主线程执行,只需将任务添加到该消息队列中:

let clickTask; TaskQueue.enqueue(clickTask) 复制代码

由于是多个线程操作同一个消息队列,所以在添加任务和取出任务时还会加上一个同步锁。

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

在 Chrome 中,跨进程之间的任务也是频繁发生的,那么如何处理其他进程发送过来的任务?你可以参考下图: 8-5.png 渲染进程专门有一个IO线程来接收其他进程传进来的信息,接收到信息之后,会把这些信息封装成任务发送给渲染主进程。后续的步骤就和之前一样了。

1.5 消息队列中的任务类型

这里面包含了很多内部消息类型,如输入事件(鼠标滚动、点击、移动)、微任务、文件读写、WebSocket、JavaScript 定时器等等。除此之外,消息队列中还包含了很多与页面相关的事件,如 JavaScript 执行、解析DOM、样式计算、布局计算、CSS 动画等。

以上这些事件都是在主线程中执行的,所以在编写 Web 应用时,你还需要衡量这些事件所占用的时长,并想办法解决单个任务占用主线程过久的问题。

当页面主线程执行完成之后,是如何退出的呢?

Chrome 是这样解决的,确定要退出当前页面时,页面主线程会设置一个退出标志的变量,在每次执行完一个任务时,判断是否有设置退出标志。如果设置了,那么就直接中断当前的所有任务,退出线程。

1.6 页面使用单线程的缺点

1.6.1 如何处理高优先级的任务

一个典型的场景是监控 DOM 节点的变化情况(节点的插入、修改、删除等动态变化),然后根据这些变化来处理相应的业务逻辑。因此DOM的变化应当首先被执行。

一个通用的设计的是利用 JavaScript设计一套监听接口,当变化发生时,渲染引擎同步调用这些接口,这是一个典型的观察者模式

这个模式存在一些问题,因为DOM通常都变化的十分频繁。每次变化都调用JavaScript接口会导致该次任务执行的时间拉长,导致执行效率的降低。如果将这种改变做成异步的消息事件添加到消息队列的尾部,又会影响到监控的实时性,因为此刻可能已经由很多任务已经在排队了。

为了权衡效率和实时性,出现了微任务。

通常我们把消息队列中的任务称为宏任务,每个宏任务中又包含一个微任务队列。在执行宏任务的过程中,如果有DOM变化,那么就会将变化添加到微任务列表中,等宏任务中的主要功能直接完成之后,渲染引擎不着急去执行下一个宏任务,而是执行当前宏任务中的微任务队列,这样就解决了实时性问题。

1.6.2 如何解决单个任务执行时间过长的问题

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

1.7 浏览器页面是如何运行的

你可以打开开发者工具,点击“Performance”标签,选择左上角的“start porfiling and load page”来记录整个页面加载过程中的事件执行情况,如下图所示: 8-7.png 我们点击展开了 Main 这个项目,其记录了主线程执行过程中的所有任务。图中灰色的就是一个个任务,每个任务下面还有子任务,其中的 Parse HTML 任务,是把 HTML 解析为 DOM 的任务。值得注意的是,在执行 Parse HTML 的时候,如果遇到JavaScript 脚本,那么会暂停当前的 HTML 解析而去执行 JavaScript 脚本。


作者:simon27
链接:https://juejin.cn/post/7036305947865808926

 伪原创工具 SEO网站优化  https://www.237it.com/ 


文章分类
代码人生
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐