浏览器的页面循环系统

消息队列和事件循环

为什么需要事件循环?

想在线程运行过程中,能接收并执行新的任务,就需要采用事件循环机制。
每个渲染进程都有一个主线程,并且主线程非常繁忙,既要处理 DOM,又要计算样式,还要处理布局,同时还需要处理 JavaScript 任务以及各种输入事件。要让这么多不同类型的任务在主线程中有条不紊地执行,这就需要一个系统来统筹调度这些任务。
在这里插入图片描述

事件循环基于两个基本准则

  • 一个处理一个事件
  • 一个任务开始后直到运行完成,不会被其他任务中断

在这里插入图片描述

全局来看,图13.1 展示了在一次迭代中, 事件循环将首先检查宏任务队列,如果宏任务等待,则立即开始执行宏任务。直到该任务运行完成(或者队列为空),事件循环将移动去处理微任务队列。如果有任务在该队列中等待,则事件循环将依次开始执行,完成一个后执行余下的微任务,直到队列中所有微任务执行完毕。注意处理宏任务和微 任务队列之间的区别:单次循环迭代中,最多处理一个宏任务 (其余的在队列中等待),而队列中的所有微任务都会被处理。
当微任务队列处理完成并清空时,(因此再微任务之前不允许从新渲染页面)事件循环会检查是否需要更新UI渲染,如果是,则会重新渲染UI视图。至此,当前事件循环结束,之后将回到最初第一个环节,再次检查宏任务队列,并开启新一轮的事件循环。
以上摘抄忍者秘籍第二版13.1深入事件循环章节

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

一个通用模式是使用消息队列。

什么是消息队列?

消息队列是一种数据结构,可以存放要执行的任务。它符合队列**“先进先出”**的特点,也就是说要添加任务的话,添加到队列的尾部;要取出任务的话,从队列头部去取。
在这里插入图片描述

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

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

在这里插入图片描述
从图中可以看出,渲染进程专门有一个 IO 线程用来接收其他进程传进来的消息,接收到消息之后,会将这些消息组装成任务发送给渲染主线程,

如何安全退出

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

页面使用单线程的缺点

第一个问题是如何处理高优先级的任务。

因为 DOM 变化非常频繁,如果每次发生变化的时候,都直接调用相应的 JavaScript 接口,那么这个当前的任务执行时间会被拉长,从而导致执行效率的下降。
如果将这些 DOM 变化做成异步的消息事件,添加到消息队列的尾部,那么又会影响到监控的实时性,因为在添加到消息队列的过程中,可能前面就有很多任务在排队了。这也就是说,
如果 DOM 发生变化,采用同步通知的方式,会影响当前任务的执行效率;如果采用异步方式,又会影响到监控的实时性。

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

微任务可以解决这个问题

什么是宏任务?什么是微任务?两者如何配合?

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

玩转计时器:延迟计时器和间隔执行

计时器能延迟一段代码的运行,延迟时长至少是指定的时长
WebAPI提供了两种创建即使器的方法:setTimeout 和 setInterval ,相对应两个清除计时器的方法:clearTimeout和clearInterval,挂载在wwindow对象,API是宿主环境提供的,不是JavaScript提供的。
这个要注意:我们只能控制计时器何时被加入队列中,而无法控制何时运行。


void ProcessTimerTask(){
  //从delayed_incoming_queue中取出已经到期的定时器任务
  //依次执行这些任务
}

TaskQueue task_queue;
void ProcessTask();
bool keep_running = true;
void MainTherad(){
  for(;;){
    //执行消息队列中的任务
    Task task = task_queue.takeTask();
    ProcessTask(task);
    
    //执行延迟队列中的任务
    ProcessDelayTask()

    if(!keep_running) //如果设置了退出标志,那么直接退出线程循环
        break; 
  }
}

从上面代码可以看出来,我们添加了一个 ProcessDelayTask 函数,该函数是专门用来处理延迟执行任务的。这里我们要重点关注它的执行时机,在上段代码中,处理完消息队列中的一个任务之后,就开始执行 ProcessDelayTask 函数。ProcessDelayTask 函数会根据发起时间和延迟时间计算出到期的任务,然后依次执行这些到期的任务。等到期的任务执行完成之后,再继续下一个循环过程。通过这样的方式,一个完整的定时器就实现了。

使用 setTimeout 的一些注意事项

  1. 如果当前任务执行时间过久,会影响定时器任务的执行
  2. 如果 setTimeout 存在嵌套调用,那么系统会设置最短时间间隔为 4 毫秒
  3. 未激活的页面,setTimeout 执行最小间隔是 1000 毫秒
  4. 延时执行时间有最大值
  5. 使用 setTimeout 设置的回调函数中的 this 不符合直觉
    如果被 setTimeout 推迟执行的回调函数是某个对象的方法,那么该方法中的 this 关键字将指向全局环境,而不是定义时所在的那个对象。

var name= 1;
var MyObj = {
  name: 2,
  showName: function(){
    console.log(this.name);
  }
}
setTimeout(MyObj.showName,1000)

第一种是将MyObj.showName放在匿名函数中执行,如下所示:


//箭头函数
setTimeout(() => {
    MyObj.showName()
}, 1000);
//或者function函数
setTimeout(function() {
  MyObj.showName();
}, 1000)

第二种是使用 bind 方法,将 showName 绑定在 MyObj 上面,代码如下所示:


setTimeout(MyObj.showName.bind(MyObj), 1000)

函数 requestAnimationFrame

使用 requestAnimationFrame 不需要设置具体的时间,由系统来决定回调函数的执行时间,requestAnimationFrame 里面的回调函数是在页面刷新之前执行,它跟着屏幕的刷新频率走,保证每个刷新间隔只执行一次,内如果页面未激活的话,requestAnimationFrame 也会停止渲染,这样既可以保证页面的流畅性,又能节省主线程执行函数的开销

requestAnimationFrame 实现的动画效果相比 setTimeout

requestAnimationFrame 提供一个原生的API去执行动画的效果,它会在一帧(一般是16ms)间隔内根据选择浏览器情况去执行相关动作。
setTimeout是在特定的时间间隔去执行任务,不到时间间隔不会去执行,这样浏览器就没有办法去自动优化

XMLHttpRequest是怎么实现的?

调用栈

每个任务在执行过程中都有自己的调用栈,那么同步回调就是在当前主函数的上下文中执行回调函数,这个没有太多可讲的。下面我们主要来看看异步回调过程,异步回调是指回调函数在主函数之外执行,
一般有两种方式:

  1. 是把异步函数做成一个任务,添加到信息队列尾部;
  2. 是把异步函数添加到微任务队列中,这样就可以在当前任务的末尾处执行微任务了。

XMLHttpRequest 运作机制

XMLHttpRequest 发起请求,是由浏览器的其他进程或者线程去执行,然后再将执行结果利用 IPC 的方式通知渲染进程,之后渲染进程再将对应的消息添加到消息队列中。
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值