由于浏览器页面的渲染进程的主线程要执行很多任务,如DOM构建、样式计算、布局计算、绘制、JavaScript执行、接收输入事件等,并且这些任务因为需要交互性来不断产生,无法事先安排,因此需要一个任务调度系统来统筹规划这些任务的执行。在浏览器中,统筹调度任务的系统就是事件循环系统(Event-Loop)
事件循环系统在于事件和循环这两个概念
事件:接收外部的输入事件,产生任务
循环:线程执行完任务后并不退出,而是循环等待输入事件产生的任务并执行
其实这两个概念使用ES6的generator函数很好阐述,同样在redux-saga中也使用了generator来监听输入事件并自动执行任务
function* eventLoop() {
// while循环保证线程循环执行
while(true) {
// yield保证了循环体在执行完watch()后停止执行
// 等待接收外部输入事件(在ES6中使用eventLoop().next()方法恢复执行)
const task = yield watch()
// 一旦接收到外部输入事件产生的任务,则执行该任务
executeTask(task)
}
}
渲染主线程需要接收外部线程的输入事件或者叫消息,执行对应的任务。如IO线程发送给主线程的事件包括有资源加载完成事件、鼠标点击事件等等,主线程接收到IO线程发送的事件,就会执行对应的任务,如资源加载完成事件执行DOM解析任务,鼠标点击事件则执行JavaScript脚本中注册的点击处理函数等。
主线程维护一个消息队列,用来存储其他线程发送的事件对应的任务。只要接收到了新的输入事件,就将对应的任务添加到消息队列中,等待主线程执行。主线程会不断循环从消息队列中取出任务并执行任务。
下面的代码解释了其中的原理,不过有点粗糙的是,主线程实际上可以在执行代码的过程中接收其他线程的事件,并将任务添加到消息队列中,下面的代码并未实现这一点,而是在清空消息队列后才会接收事件,这里要注意一下。
class Queue {
constructor () {
// event与task映射关系表
this.eventMapToTask = {
// 存储了event与对应的task
}
// 主线程维护的消息队列,存储event对应的task
this.taskQueue = []
}
// 添加任务到队列
addTask(task) {
this.queue.push(task)
}
// 从队列中取出任务
takeTask() {
const task = this.queue.shift()
return task
}
}
function* eventLoop() {
// 实际上,这里应该保证queue是单例,为了简化,这里就不设置单例模式了
const queue = new Queue()
// while循环保证线程循环执行
while(true) {
// yield保证了循环体在执行完watch()后停止执行
// 等待接收外部输入事件(在ES6中使用eventLoop().next()方法恢复执行)
// 接收其他线程发送的事件
const event = yield watch()
// 将event对应的task加入队列中
queue.addTask(queue.eventMapToTask[event])
// 清空任务队列中的任务
while(queue.taskQueue.lenght > 0) {
// 取出队列中的任务
const task = queue.takeTask()
// 执行任务
executeTask(task)
}
}
}
在接收事件的这个过程实际上还有跨进程通信的参与,因为事件最开始是从浏览器的网络进程、浏览器主进程传递而来。
因此,实际上由IO线程会维护消息队列更合适。这样上面的代码中,主线程可以不管理接收事件,只要不断循环执行消息队列取出的任务即可。
主线程会设定一个退出的标志变量,在每次执行完一个任务时,检查退出标志变量。
在确定要退出页面时,退出标志变量为true,则中断当前任务,退出当前的事件循环,退出主线程。
如何衡量效率和实时性?
在V8引擎创建全局执行上下文时,同时在内部创建一个微任务队列。在执行当前宏任务的过程中,产生的微任务则存储到当前宏任务绑定的微任务队列中。
每个宏任务都会关联自己的微任务队列。
消息队列中的任务,如
系统对于宏任务的调度策略不能满足对时间精度要求较高的需求
浏览器的定时器API使用了setTimeout 和 setInterval,将任务延迟一段时间后添加到延迟队列中。
将一个函数作为参数传递给另一个函数,那作为参数的函数就称为回调函数
回调函数在主函数返回之前执行,则为同步回调函数,同步回调函数在当前函数的执行栈中调用。
回调函数在主函数的外部执行,称为异步回调函数,异步回调的调用分为两种
在实现JavaScript高性能流畅动画的场景下,需要使用requestAnimationFrame这个API而不是setTimeout,requestAnimationFrame可以精确控制动画在每一帧渲染,而不会因为定时器的不精确导致帧率降低。
这里将消息队列与延迟队列暂时作为一个队列来看待。
当队头任务耗时长,且优先级低时,高优先级任务得不到实时响应,就会导致页面不流畅,用户体验变差的情况。
比如在交互阶段,下面几种任务都应该视为高优先级的任务:
可以引入多个队列,按优先级排序,将任务划分为不同的优先级,添加到不同的队列中。
按优先级的高低依次执行任务。
但这里有个缺点就是,这种划分打乱了任务产生的相对顺序
为不同类型的任务创建不同优先级的消息队列,比如:
但这里还需要进行微调,因为如果一直有新的高优先级任务加入队列,则其他低优先级队列得不到执行,称为任务饿死。
为队列设置了执行权重,在连续执行了一定个数的该优先级队列的任务后,中间会执行一次低优先级任务,缓解任务饿死。
通常情况下,显示器从显卡的前缓冲区读取图片的频率与浏览器生成新图像到后缓冲区的频率不一致,或者说不同步,这会导致很多问题
为了解决这些问题,就需要将显示器的时钟同步周期和浏览器生成页面的周期绑定起来。本质上,就是显示器与浏览器使用一个同步信号VSync来互相通知,绑定周期。