每每遇到异步问题的时候,总是能让我们抓耳挠腮,无所适从。那么我们就来讲讲异步问题——EventLoop事件循环机制。首先来了解一个概念,进程和线程:
进程:进程是 CPU 资源分配的最小单位;
线程:线程是 CPU 调度的最小单位。
如何理解这句话呢?计算机是多进程执行的,打开自己的电脑的任务管理器,可以看到所有的执行的进程,而每一个执行的进程又是有多个线程组成的。线程是一个进程中代码的不同执行路线。一个进程的内存空间是共享的,每个线程都可用这些共享内存。而每个进程是独立的。
所以这里就会出现另一个问题:多进程和多线程的问题。
多进程:进程之前是相互独立的,是互相不影响的。我们打开电脑,听着歌,敲着代码,互不干扰,这就是多进程。
多线程:一个进程是由多个线程组成的,也就是说,想要完成一件事情,需要分多步来做。例如,当打开一个网页的时候,可能需要解析js,css,html,需要将他们渲染到页面上,需要发起网络请求等,这就是一个个线程。当完成了他们自己的使命,那么线程也就被销毁了。
所以我们要讲的就是关于异步或者eventloop就是关于线程的相关内容。有了进程和线程的了解,我们就该让js在线程中运行起来。那么js的运行,其实是要分俩步走的。
- 编译并执行 JavaScript 代码,完成内存分配、垃圾回收等;(执行引擎)
- 为 JavaScript 提供一些对象或机制,使它能够与外界交互。(执行环境)
chrome和node都提供了v8执行引擎,但是他们的执行环境不一样。接下来,我们再来讲三个概念:
堆:堆是一种数据结构,是利用完全二叉树维护的一组数据,堆分为两种,一种为最大堆,一种为最小堆,将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。堆是线性数据结构,相当于一维数组,有唯一后继。
栈:栈在计算机科学中是限定仅在表尾进行插入或删除操作的线性表。 栈是一种数据结构,它按照后进先出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读数据的时候从栈顶开始弹出数据。栈是只能在某一端插入和删除的特殊线性表。
队列:特殊之处在于它只允许在表的前端(front
)进行删除操作,而在表的后端(rear
)进行插入操作,和栈一样,队列是一种操作受限制的线性表。
进行插入操作的端称为队尾,进行删除操作的端称为队头。 队列中没有元素时,称为空队列。
队列的数据元素又称为队列元素。在队列中插入一个队列元素称为入队,从队列中删除一个队列元素称为出队。因为队列只允许在一端插入,在另一端删除,所以只有最早进入队列的元素才能最先从队列中删除,故队列又称为先进先出。
ok,准备知识就说到这里,我们接入正题:eventLoop;
我们知道,在 JavaScript 运行的时候,JavaScript Engine 会创建和维护相应的堆(Heap)和栈(Stack),同时通过 JavaScript 的执行环境提供的一系列 API(例如 setTimeout、XMLHttpRequest 等)来完成各种各样的任务。
JavaScript 是一种单线程的编程语言,只有一个调用栈,决定了它在同一时间只能做一件事情。在 JavaScript 的运行过程中,真正负责执行 JavaScript 代码的始终只有一个线程,通常被称为主线程,各种任务都会用排队的方式来同步执行。这种方式最常见的一个问题就是:如果你尝试执行一段非常耗时的同步代码,浏览器就没办法同时去渲染 GUI,导致界面失去响应,也就是被阻塞了。然而 JavaScript 却又是一个非阻塞(Non-blocking)、异步(Asynchronous)、并发式(Concurrent)的编程语言,这就得说说 JavaScript 的事件循环(Event Loop)机制了。
事件循环(Event Loop) 是让 JavaScript 做到既是单线程,又绝对不会阻塞的核心机制,也是 JavaScript 并发模型(Concurrency Model)的基础,是用来协调各种事件、用户交互、脚本执行、UI 渲染、网络请求等的一种机制。一句话,Event Loop 只不过是实现异步的一种机制而已。
Event Loop 分为两种,一种存在于 Browsing Context 中,还有一种在 Worker 中。
- Browsing Context 是指一种用来将 Document 展现给用户的环境。例如浏览器中的 tab,window 或 iframe 等,通常都包含 Browsing Context。
- Worker 是指一种独立于 UI 脚本,可在后台执行脚本的 API。常用来在后台处理一些计算密集型的任务。
我们重点讲解Browsing Context.另外,Event Loop 并不是在 ECMAScript 标准中定义的,而是在 HTML 标准中定义的。在 JavaScript 执行环境中(以 V8 为例),只是实现了 ECMAScript 标准,而并不关心什么 Event Loop。也就是说 Event Loop 是属于 JavaScript 的执行环境 的,是由宿主环境提供的(比如浏览器)。这也是为什么浏览器和node中的EventLoop是有区别的。所以千万不要搞错了,这也是前面介绍 JavaScript 执行引擎 和 执行环境 的原因。
EventLoop中的任务队列
在执行和协调各种任务时,Event Loop 会维护自己的任务队列。任务队列又分为 异步队列 和 同步队列 两种。
异步队列
一个 Event Loop 会有一个或多个 异步队列,这是一个先进先出(FIFO)的有序列表,存放着来自不同任务源的任务。HTML标准规定:用户交互,ajax请求,鼠标、键盘事件,数据操作,setTimeout,setInterval等等都属于异步源,所有来自这些 都会被放到对应的异步任务队列中等待处理。
注意:
- 来自相同 异步任务源的 任务,必须放在同一个 异步队列中;
- 来自不同异步任务源的 Task,可以放在不同的 异步队列 中;
- 同一个 异步队列内的 Task 是按顺序执行的;
- 但对于不同的 异步队列(Task Source),浏览器会进行调度,允许优先执行来自特定 异步源的 Task。
同步任务队列
同步任务队列和异步队列类似,也是一个存放不同任务任务源任务的有序列表,所不同的是一个EventLoop只会有一个同步任务队列。HTML标准中并没有具体规定哪些是同步任务,但是一般以类似于promise,MutationObserver等不是异步任务的都是同步任务。
JavaScript运行环境的执行机制
1. 主线程不断循环;
2. 对于同步任务,创建执行上下文(Execution Context),按顺序进入执行栈;
3. 对于异步任务:
- 与步骤 2 相同,同步执行这段代码;
- 将相应的 Task(或 Microtask)添加到 Event Loop 的任务队列;
- 由其他线程来执行具体的异步操作。
其他线程是指:尽管 JavaScript 是单线程的,但浏览器内核是多线程的,它会将 GUI 渲染、定时器触发、HTTP 请求等工作交给专门的线程来处理。
另外,在 Node.js 中,异步操作会优先由 OS 或第三方系统提供的 异步接口来执行,然后才由线程池处理。
4. 当主线程执行完当前执行栈中的所有任务,就会去读取 Event Loop 的任务队列,取出并执行任务;
5. 重复以上步骤。
大概是这样的:
Event Loop 处理模型
EventLoop作为处理事件循环每次的循环也是相当的复杂。主要是分为三个步骤:
- 执行异步任务:从异步队列中取出最老的一个 Task 并执行;如果没有 Task,直接跳过。
- 执行 同步任务:遍历同步任务队列并执行所有同步任务(参考 Perform a microtask checkpoint)。
- 进入 Update the rendering(更新渲染)阶段:
- 设置 Performance API 中 now() 的返回值。Performance API 属于 W3C High Resolution Time API 的一部分,用于前端性能测量,能够细粒度的测量首次渲染、首次渲染内容等的各项绘制指标,是前端性能追踪的重要技术手段,感兴趣的同学可关注。
- 遍历本次 Event Loop 相关的 Documents,执行更新渲染。在迭代执行过程中,浏览器会根据各种因素判断是否要跳过本次更新。
- 当浏览器确认继续本次更新后,处理更新渲染相关工作:
- 触发各种事件:Resize、Scroll、Media Queries、CSS Animations、Fullscreen API。
- 执行 animation frame callbacks,window.requestAnimationFrame 就在这里。
- 更新 intersection observations,也就是 Intersection Observer API(可用于图片懒加载)。更新渲染和 UI,将最终结果提交到界面上。
至此,Event Loop 的一次循环结束:
同步任务的执行机制
同步任务会在第 2 步时被执行。实际上按照 HTML 标准,在以下几种情况中同步任务都会被执行:
- 某个 Task 执行完毕时(即上述情况)。
- 进入脚本执行(Calling scripts)的清理阶段(Clean up after running script)时。
- 创建和插入节点时。
- 解析 XML 文档时。
同时,在当前 Event Loop 轮次中动态添加进来的同步任务,也会在本次 Event Loop 循环中全部执行完(上图其实已经画出来了)。
最后一定要注意的是,执行同步任务是有前提的:当前执行栈必须为空,且没有正在运行的执行上下文。否则,就必须等到执行栈中的任务全部执行完毕,才能开始执行同步任务。JavaScript 会确保当前执行的同步代码不会被同步任务打断。
就写这么多吧,有遗漏,以后再有补充。。。。。