浏览器事件循环的前世今生

64 阅读5分钟

第一问:为什么会出现事件循环,它解决了什么问题

主要是解决了在JavaScript单线程环境中处理多任务的问题。JavaScript最初的设计目标就是作为浏览器的一种动态脚本语言,用于实现网页的复杂交互。但JavaScript运行在浏览器的单一主线程中,它不能同时进行多个操作,否则会引发线程冲突和数据不一致等问题。

但是,浏览器环境本身是一个多线程的环境。用户交互、网络请求、定时器等多种事件都会产生对应的任务,这些任务需要在多线程环境中得到有序、高效的处理。因此,事件循环机制应运而生。它通过创建一个事件队列,将各种事件产生的任务放入队列中等待执行。主线程在执行完当前的任务后,会检查事件队列中是否有待处理的任务,如果有,就取出任务继续执行。这个过程不断重复,形成了一个循环,即事件循环。

总的来说,事件循环机制是JavaScript为了适应浏览器多线程环境,同时保持其单线程执行模型的一种解决方案。它使得JavaScript能够在保证线程安全的同时,高效地处理各种事件和任务。

第二问:代码会被压入执行栈,那事件循环具体是如何与渲染线程进行配合的呢 当JavaScript代码执行时,它会被压入执行栈中。执行栈是一个后进先出(LIFO)的数据结构,用于跟踪当前正在执行的函数。当函数执行完成后,它会被从执行栈中弹出。

事件循环的主要任务是不断地检查执行栈是否为空,以及任务队列中是否有待处理的任务。如果执行栈为空,并且任务队列中有任务事件循环就会从队列中取出一个任务,并将其推入执行栈中执行。

事件循环与渲染的配合

事件循环与渲染线程之间的配合是通过浏览器提供的机制来实现的。具体来说:

  1. 任务执行与渲染机会:当JavaScript代码在执行栈中执行时,渲染线程是被阻塞的。但是,每当执行栈为空(即没有正在执行的JavaScript代码)时,浏览器就会得到一个渲染的机会。这时渲染线程会开始执行渲染流程,更新页面的内容。
  2. 微任务与宏任务: 在执行栈为空后,事件循环会首先检查微任务队列(如Promise的回调函数)。如果有微任务,它们会被执行,直到微任务队列清空。然后,浏览器再次得到一个渲染的机会。之后,事件循环会检查宏任务队列(如setTimeout、事件回调等),并取出队列中的第一个任务执行。这个过程不断重复。
  3. requestAnimationFrame与渲染循环requestAnimationFrame函数允许开发者在浏览器的下一次重绘之前执行代码。这提供了一个与浏览器渲染循环同步的机制。当调用requestAnimationFrame时,它的回调函数会被放入一个特殊的任务队列中,等待浏览器在下次重绘之前执行。这确保了动画的平滑性和与屏幕刷新率的同步。

一句话总结:事件循环负责执行JavaScript任务,并在执行栈为空时给浏览器提供渲染的机会;

第三问:一个执行栈执行时间太久,那么就会导致掉帧?另外,如何理解执行栈?

如果执行栈中的代码执行时间太久,确实会导致掉帧的问题。执行栈中的代码执行是同步的,如果某段代码执行时间过长,它会阻塞主线程,进而阻塞浏览器的渲染流程,这就会导致页面无法及时更新,出现掉帧现象。

每个执行栈的区分主要是基于函数调用。在JavaScript中,当调用一个函数时,该函数会在执行栈中创建一个新的执行上下文。这个执行上下文包含了函数内部的变量、作用域链等信息,并且成为当前正在执行的代码。当函数执行完毕后,其对应的执行上下文会从执行栈中弹出,控制权返回给之前的执行上下文。

第四问:每个执行栈执行的内容都是来自任务队列,那么栈中就是宏\微任务的执行上下文?

是的, 事件循环的工作机制:当执行栈为空时,事件循环会首先检查微任务队列,如果有微任务,就会连续不断地执行微任务,直到微任务队列清空。然后,事件循环才会从宏任务队列中取出一个宏任务放入执行栈中执行。这个过程会不断重复,形成事件循环。

第五问:如果执行栈中是微任务执行上下文,那么执行结束后,还会检查是否需要渲染吗

当执行栈中的宏任务执行上下文执行结束后,浏览器确实会检查是否有渲染需求。这是因为宏任务(如setTimeout、事件监听器的回调等)通常是由宿主环境(如浏览器)发起的,它们之间的切换可能会涉及到较大的资源消耗,因此浏览器会在每个宏任务执行完毕后给予页面一个渲染的机会。

而对于微任务,情况稍有不同。微任务(如Promise的回调、process.nextTick等)是由JavaScript引擎自身发起的,它们会在当前宏任务执行完毕后立即执行。由于微任务是在单个宏任务之后立即执行的,所以它们之间的切换相对较快,通常不会直接触发渲染。