浏览器事件循环机制

        JavaScript语言最大的特点是单线程(JavaScript其实没有线程概念,针对其不具备并行任务处理的特性,称之为单线程),作为浏览器脚本语言,它主要用途是与用户互动的DOM操作。若以多线程方式操作DOM,回带来复杂的同步问题,例如线程1在某个DOM节点上添加内容,线程2则删除这个节点,浏览器以那个结果为准?

HTML提出了Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。新标准利用了多核CPU的计算能力,但是并没有改变JavaScript单线程的本质

        早期网页的交互简单,同一时间只能执行一段代码,也能满足使用的需求。随着交互的多样和复杂程度的提高,浏览器已经发展成多进程+多线程架构。通常每个网页会创建一个独立的渲染进程(同一站点会合并进程),渲染进程中也是包含多线程,其主线程非常繁忙(浏览器中JavaScript引擎是运行在渲染进程的主线程上),处理DOM、计算样式、处理布局、执行JavaScript任务和各种输入事件等都需要再主线程上完成,如此多不同类型任务有条不紊的执行就需要一个系统来统筹调度。浏览器引入事件循环来协调事件、用户操作、脚本执行、渲染、网络请求等任务的执行,通过事件循环浏览器利用任务队列来管理任务,利用异步事件让代码非阻塞的执行。

阻塞和非阻塞的区别:阻塞和非阻塞指的是调用者(程序)在等待返回结果(或输入)时的状态。阻塞时,在调用结果返回前,当前线程会被挂起,并在得到结果之后返回。非阻塞时,如果不能立刻得到结果,则该调用者不会阻塞当前线程。因此对应非阻塞的情况,调用者需要定时轮询查看处理状态

什么是事件循环(Event Loop)?

"Event Loop是一个程序结构,用于等待和发送消息和事件。(a programming construct that waits for and dispatches events or messages in a program.)"—— 维基百科

        Event Loop可以理解为一个消息分发器,通过接受和分发不同类型的消息,让执行程序的事件调度更加合理。需要明确JavaScript没有自己的事件循环系统,浏览器事件循环是以浏览器为宿主环境实现的事件调度,也就是渲染进程提供的循环系统。执行顺序如下[1]:

  1. 执行同步代码;
  2. 执行一个任务(执行栈中没有,就会从任务队列中获取);
  3. 执行过程如果遇到微任务,就将它添加到微任务的队列;
  4. 任务执行完毕后,立即执行当前微任务队列的所有微任务(依次执行);
  5. 当前任务执行完毕,开始检查渲染,然后渲染进程接管进行渲染;
  6. 渲染完毕,JavaScript线程继续接管,开始下一个循环。

浏览器事件循环机制_第1张图片

        事件循环采用了系统级中断机制,当有事件需要处理时线程被激活,反之线程挂起,所以并不会导致主线程卡死。[2]

任务与微任务

        上面我们知道浏览器中代码执行的会分成任务,并通过事件循环来协调。如果任务同步执行,一些任务时间过长造成阻塞,将导致一些问题。例如DOM频繁操作时,若每次都调用相应JavaScript接口,当前任务执行时间会被拉长,执行效率下降;若将DOM变化当做异步的任务(asynchronous),存放在任务队列的尾部,队列前部的较多任务又会影响监控的实时性。如何权衡效率和实时性?

        任务队列应用而生,每个任务(现在已经没有宏任务的说法,参考MDN上概念描述)都有一个微任务(microtask)队列,会在任务主要功能执行完成后清空微任务队列,参考上面步骤4。由此,浏览器任务分为同步任务、异步任务和微任务。

        根据W3C最新解释[3]:

        每个任务都有一个任务类型,同一个任务类型的任务必须在一个队列,不同类型的任务可以分属不同的队列;在一次事件循环中,浏览器可以根据实际情况从不同的队列取出任务执行。浏览器必须准备好一个微任务队列,微任务队列的任务优先于其他所有的任务执行(任务没有优先级,任务队列有优先级)。

        实际实现中浏览器不可能为所有类型的任务创建队列,以chrome为例至少包含下面三个队列:

  • 延时队列:优先级中,存放定时器回调任务,例如setTimeout、setInterval、script、I/O操作、UI事件,setImmediate、requestAnimationFrame等;
  • 交互队列:优先级高,存放用户操作后产生的事件任务;
  • 微任务队列:优先级最高,存放需要尽快执行的任务,例如Promise、async/await、MutationObserver;

浏览器是基于多进程+多线程架构,由于多个线程操作同一个任务队列存在线程同步问题,所以添加任务和取出任务时还需要添加同步锁(多个线程同时运行,线程的调度由操作系统决定,程序本身无法决定。任何一个线程都有可能在任何指令被操作系统暂停,然后在某个时间段继续执行。因此多线程同时读取共享变量,会存在数据不一致的问题,例如JAVA中通过关键字synchronized设置同步锁)

不记得出处了

setTimeout定时器

        setTimeout作为最常用的Web API,常用于创建定时器任务。通常浏览器每次执行完一个任务后,都会计算是延时队列否有定时器任务到期,会将到期所有定时器的任务执行掉,再开始下次循环过程!

延时队列:其实是一个hashmap结构,等到执行这个结构的时候,会计算hashmap中的每个任务是否到期了,到期了就去执行,直到所有到期的任务都执行结束,才会进入下一轮循环。

        使用setTimeout需注意:

  • 当前任务执行过久,会导致定时器任务不能按照设定的时间被执行;
  • 当定时器任务被嵌套调用,系统会设置最短的时间间隔为4ms(可能嵌套电泳5次以上);
  • 未激活的页面(多tab后台运行的页面),setTimeout执行最小间隔是1000毫秒;
  • 延迟执行时间有最大值,溢出相当于延时0ms,32bit除去符号位最大值 2147483647 (约 24.8 天);

        由于setTimeout的实时性不好,一些场景并不适合使用setTimeout。例如,若要实现流畅的动画效果,requestAnimationFrame (简称raf)是更好的选择。使用 raf不需要设置具体的时间,由系统来决定回调函数的执行时间,raf里面的回调函数是在页面刷新之前执行,它跟着屏幕的刷新频率走,保证每个刷新间隔只执行一次,如果页面未激活的话,raf 也会停止渲染,这样既可以保证页面的流畅性,又能节省主线程执行函数的开销 。raf 提供一个原生的API去执行动画的效果,它会在一帧(一般是16ms)间隔内根据选择浏览器情况去执行相关动作。raf的回调函数也是在主线程上执行的,如果其中的一个回调函数执行过久,会影响到其他的任务的[4]。

总结

        阐述了浏览器的循环机制, 事件循环,任务队列,微任务是不同的机制:

  • 事件循环,解决主线程不能接收任务的问题。
  • 任务队列,解决众多任务,按什么顺序被主线程接受的问题。
  • 微任务,解决临时产生的高优先级任务,无法被优先执行的问题。

此外,比较了setTimeout 和requestAnimationFrame两个Web API。

[1] 浏览器事件循环 https://febook.hzfe.org/awesome-interview/book3/browser-event-loop#2-浏览器为什么需要事件循环

[2] 消息队列和事件循环:页面是怎么“活”起来的?https://time.geekbang.org/column/article/132931?cid=100033601

[3] 浏览器事件循环 https://juejin.cn/post/7259927532249710653?searchId=20230731095843F8F42B37C59C7FA74E06#headin

[4] WebAPI:setTimeout是如何实现的?https://time.geekbang.org/column/article/134456

你可能感兴趣的:(JS,探究,前端,javascript,chrome)