关于EventLoop

关于EventLoop

不知道有没有人跟我我一样产生过这样的疑问

1、浏览器的一次事件循环♻️的时间是多久?是16.67ms吗?

2、我当前的这行代码执行的时候处于当前循环的什么位置? 是循环的开始?还是中间位置,还是循环马上要结束了?

我大概知道产生这样疑问,说明对对浏览器内部event loop的心智模型是不对的,但是很长一段时间以来,无论是在看vue中的nextTick还是一些文章中介绍的“下次dom更新。。“这样的字眼时心中总是感觉有一层不甚明确的概念,What exactly is a event loop tick?

现在理解如下:

当浏览器解析到一个script开始标签时,整个这个标签整体可以看作是对应一次Tick

<script>
    // Tick开始
    console.log('同步代码')
    ...
    setTimeout(() => {
        console.log('宏任务队列代码')
    })
    ...
    Promise.then(() => {
        console.log('微任务队列代码')
    })
    requestAnimationFrame(() => {
        console.log('本次微任务之后下次宏任务之前的代码。。')
    })
    ...
    // Tick结束
</script>

浏览器在执行这段代码的时候,浏览器会为其开辟一块儿内存空间作为程序的执行栈,整块代码会作为一个main调用最先进入执行栈开始执行,可以认为此时开启了一个宏任务,浏览器会依次执行该栈中的函数,遇到同步代码就执行,遇到setTimeout这样的Webapi调用,会将其压入宏任务队列中下次Tick再执行,遇到Promise.then这样的代码,它属于是本次Tick运行中产生的微任务代码,会将其放入微任务队列,在本次Tick结束之前(即所有的同步代码都执行完毕)会对其进行调用。也即是浏览器的每一个Tick会执行一个宏任务以及在该宏任务执行期间产生微任务。(自己产生的微任务,自己执行掉,不会留到下一个Tick去执行)

而requestAnimationFrame是个比较特殊的调用,它根本不在event loop的生命周期里,这个api也并不属于JS运行时,而是浏览器宿主环境提供的,它属于另一个主题——渲染。

浏览器作为一个复杂的应用是多线程工作的,除了运行JS的线程外,还有渲染线程,定时器触发线程,HTTP 请求线程等等。JS线程可以读取并且修改DOM而渲染线程也需要读取DOM,这是一个典型的多线程竞争临界资源的问题。 所以浏览器就把这两个线程设计成互斥的,即同时只能有一个线程在执行。

渲染原本就不应该出现在event loop相关的知识体系里。因为event loop显然是在讨论JS如何运行的问题,而渲染则是浏览器另外一个线程的工作。但是requestAnimationFrame的出现却把这两件事情给关联起来不在V8环境里,只是浏览器又开放的一个在渲染之前发生的新的hook。所以它对应的animation callback queue的处理方式也是不一样的。
通过调用requestAnimationFrame我们可以在下次渲染之前执行回调函数。那下次渲染具体是哪个时间点呢?渲染和event loop有什么关系呢?简单来说,就是在每一次event loop的末尾,判断当前页面是否处于渲染时机,是就重新渲染。而渲染时机又是个啥,查阅资料发现有点“玄学”~ 有屏幕的硬件限制,跟机器性能有关联,跟js执行是否超时有关联,总之要不要渲染是一个多方面因素条件综合下来浏览器判断的结果。有点React中shouldComponentUpdate内味儿~但无论如何requestAnimationFrame保证的就是在浏览器下次渲染之前一定会被调用。

关于执行上下文

函数调用栈,其实就是执行上下文栈,每当调用一个函数

时就会产生一个新的执行上下文,同时新产生的这个执行

上下文就会被压入执行上下文栈中。

全局上下文最先入栈,并且在离开页面时才会出栈,js引擎

不断的执行上下文栈中栈定的那个执行上下文,在它执行完毕后

将它出栈,直到整个执行栈为空。

关于执行栈有5点注意:

  • 单线程
  • 同步执行
  • 只有一个全局上下文
  • 可以有无数个函数上下文(理论是函数上下文没有限制,但是太多了会爆栈)
  • 每个函数调用都会创建一个新的执行上下文

明确一个概念: 函数上下文执行栈是与js引擎相关的概念,

而异步/回调是与运行环境相关的概念。

如果执行栈与异步机制完全无关,我们写了无数遍的点击触发回调是如何做到的呢?是运行环境(浏览器/Node)来完成的, 在浏览器中,异步机制是借助 event loop 来实现的,

event loop 是异步的一种实现机制。JavaScript 引擎只是“傻傻”的一直执行栈顶的函数,而运行环境负责管理在什么时候压入执行上下文栈什么函数来让引擎执行

JavaScript 引擎本身并没有时间的概念,只是一个按需执行 JavaScript 任意代码片段的环境。“事件”( JavaScript 代码执行)调度总是由包含它的环境进行。

另外,从一个侧面可以反应出执行上下文栈与异步无关的 —— 执行上下文栈是写在 ECMA-262 的规范中,需要遵守它的是浏览器的 JavaScript 引擎,比如 V8、Quantum 等。

event loop 的是写在 HTML 的规范中,需要遵守它的是各个浏览器,比如 Chrome、Firefox 等。

浏览器和浏览器的js引擎是两码事。

一个页面有一个event loop ,但是一个event loop 可以有多个

task queues.

每个来自相同 task source 并由相同 event loop(比如,Document 的计时器产生的回调函数,Document 的鼠标移动产生的事件,Document 的解析器产生的 tasks) 管理

的 task 都必须加入到同一个 task queue 中,可是来自不同 task sources 的 tasks 可能会被排入到不同的 task queues 中。

来自相同的 task source 的 task 将会被排入相同的 task queue,但是规范说来自不同 task sources 的 tasks 可能会被排入到不同的 task queues 中,也就是说一个 task queue 中

可能排列着来自不同 task sources 的 tasks,但是具体什么 task source 对应什么 task queue,规范并没有具体说明。

一般我们看个各个文章中对于 task queue 的描述都是只有一个,不论是网络,用户时间内还是计时器都会被 Web APIs 排入到用一个 task queue 中,但事实上规范中明确表示了是

有多个 task queues,并举例说明了这样设计的意义:

举例来说,一个用户代理可以有一个处理键盘鼠标事件的 task queue(来自 user interaction task source),还有一个 task queue 来处理所有其他的。用户代理可以以 75% 的几率

先处理鼠标和键盘的事件,这样既不会彻底不执行其他 task queues 的前提下保证用户界面的响应, 而且不会让来自同一个 task source 的事件顺序错乱

当用户代理将要排入任务时,必须将任务排入相关的event loop 的task queues;

这句话很关键,是用户代理来控制任务的调度.

参考链接🔗:

理解event loop的n重境界