消息队列和事件循环:页面是怎么“活”起来的

消息队列:先进先出

image.png

浏览器中如何处理其他进程发送过来的任务
image.png

消息队列中的任务类型

内部消息类型:如 输入事件(鼠标滚动,点击,移动),微任务,文件读写,WebSocket,JavaScript定时器
页面相关事件:JavaScript执行,解析DOM,样式计算,布局计算,CSS动画等

页面使用单线程的缺点

页面线程所有执行的任务都来自于消息队列,消息队列是“先进先出”的属性,也就是说放入队列中的任务,需要等待前面的任务被执行完,才会被执行。

问题一:如何处理高优先级的任务
一个典型的场景是监控DOM节点的变化情况(节点的插入,修改,删除等动态变化),然后根据这些变化来处理相应的业务逻辑。那么出现的问题就是,如果DOM发生变化,采用同步通知的方式,会影响当前任务的执行效率,如果采用异步方式,又会影响到监控的实时性。

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

问题二:如何解决单个任务执行时长过久的问题

因为所有的任务都是在单线程中执行的,所以每次只能执行一个任务,而其他任务就处于等待状态,如果其中一个任务执行时间过久,那么下一个任务就要等待很长时间。
image.png
对于这种情况,javascript可以通过回调功能来规避这种问题,也就是让要执行的javascript任务滞后执行。

WebAPI:setTimeout是如何实现的?

浏览器怎么实现setTimeout

事件循环系统:渲染进程中所有运行在主线程上的任务都需要添加到消息队列,然后事件循环系统再按照顺序执行消息队列中的任务。下面典型的事件
当接收到了HTML文档数据,渲染引擎就会“解析DOM”事件添加到消息队列中
当用户改变了Web页面的窗口大小,渲染引擎就会将“重新布局”的事件添加到消息队列中
当触发了JavaScript引擎垃圾回收机制,渲染引擎会将“垃圾回收”任务添加到消息队列中
如果要执行一段异步JavaScript代码,也是需要将执行任务添加到消息队列中


在Chrome中除了正常使用的消息队列之外,还有另一个消息队列,这个队列中维护了需要延迟执行的任务列表,包括了定时器和Chromium内部一些需要延迟执行的任务。所以当通过 JavaScript 创建一个定时器时,渲染进程会将该定时器的回调任务添加到延迟队列中。

当通过 JavaScript 调用 setTimeout 设置回调函数的时候,渲染进程将会创建一个回调任务,包含了回调函数 showName、当前发起时间、延迟执行时间,其模拟代码如下所示:

  1. struct DelayTask{
  2. int64 id
  3. CallBackFunction cbf;
  4. int start_time;
  5. int delay_time;
  6. };
  7. DelayTask timerTask;
  8. timerTask.cbf = showName;
  9. timerTask.start_time = getCurrentTime(); // 获取当前时间
  10. timerTask.delay_time = 200;// 设置延迟执行时间

创建好回调任务之后,再将该任务添加到延迟执行队列中,代码如下所示:

  1. delayed_incoming_queue.push(timerTask)

现在通过定时器发起的任务就被保存到延迟队列中了,接下来看看消息循环系统是怎么触发延迟队列的。

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

设置一个定时器,JavaScript 引擎会返回一个定时器的 ID。那通常情况下,当一个定时器 的任务还没有被执行的时候,也是可以取消的,具体方法是调用clearTimeout 函数,并传 入需要取消的定时器的 ID。如下面代码所示:

  1. clearTimeout(timer_id)

其实浏览器内部实现取消定时器的操作也是非常简单的,就是直接从 delayed_incoming_queue 延迟队列中,通过 ID 查找到对应的任务,然后再将其从队列 中删除掉就可以了。

WebAPI:XMLHttpRequest是怎么实现的?

回调函数VS系统调用栈

回调函数:将一个函数作为参数传递给另一个函数,那作为参数的这个函数就是回调函数。

同步回调
我们将一个匿名函数赋值给变量 callback,同时将 callback 作为参数 传递给了 doWork() 函数,这时在函数 doWork() 中 callback 就是回调函数。上面的回调方法有个特点,就是回调函数 callback 是在主函数 doWork 返回之前执行的, 我们把这个回调过程称为同步回调

  1. let callback = function(){
  2. console.log('i am do homework')
  3. }
  4. function doWork(cb) {
  5. console.log('start do work')
  6. cb()
  7. console.log('end do work')
  8. }
  9. doWork(callback)

异步回调
在这个例子中,我们使用了 setTimeout 函数让 callback 在 doWork 函数执行结束后,又 延时了 1 秒再执行,这次 callback 并没有在主函数 doWork 内部被调用,我们把这种回 调函数在主函数外部执行的过程称为异步回调

  1. let callback = function () {
  2. console.log("i am do homework");
  3. };
  4. function doWork(cb) {
  5. console.log("start do work");
  6. setTimeout(cb, 1000);
  7. console.log("end do work");
  8. }
  9. doWork(callback)