一道关于js的事件循环机制的问题?

引子:为什么会有事件循环

重点: javascript 从诞生之日起就是一门单线程的非阻塞的脚本语言

  • 单线程:JavaScript 是单线程的,单线程是指 JavaScript 引擎中解析和执行 JavaScript 代码的线程只有一个(主线程),每次只能做一件事情。单线程存在是必然的,在浏览器中, 如果 javascript 是多线程的,那么当两个线程同时对 dom 进行一项操作,例如一个向其添加事件,而另一个删除了这个 dom,这个时候其实是矛盾的
  • 非阻塞:当我们的 Javascript 代码运行一个异步任务的时候(像 Ajax 等),主线程会挂起这个任务,然后异步任务返回结果的时候再根据特定的结果去执行相应的回调函数

如何做到非阻塞呢?这就需要我们的主角——事件循环(Event Loop

我们看一个很经典的图,这张图基本可以概括了事件循环(该图来自演讲—— 菲利普·罗伯茨:到底什么是Event Loop呢?| 欧洲 JSConf 2014[1])后面演示用的 Loupe[2] 也是该演讲者写的((Loupe是一种可视化工具,可以帮助您了解JavaScript的调用堆栈/事件循环/回调队列如何相互影响))

javascript 代码执行的时候会将不同的变量存于内存中的不同位置:堆(heap)和栈(stack)中来加以区分。其中,堆里存放着一些对象。而栈中则存放着一些基础类型变量以及对象的指针

stack):当我们调用一个方法的时候,js会生成一个与这个方法对应的执行环境(context),又叫执行上下文。这个执行环境中存在着这个方法的私有作用域,上层作用域的指向,方法的参数,这个作用域中定义的变量以及这个作用域的this对象。而当一系列方法被依次调用的时候,因为js是单线程的,同一时间只能执行一个方法,于是这些方法被排队在一个单独的地方。这个地方被称为执行栈

比如,如下是一段同步代码的执行

node 是基于谷歌v8 javascript引擎的 非阻塞事件驱动 平台, 接下来的一系列文章,我将描述什么是事件循环,它是如何工作的,它如何影响我们的应用程序。

nodejs 的事件驱动模型涉及 Event DemultiplexerEvent Queue 。所有I/O请求最终将生成完成/失败事件或任何其他触发器,统称为 事件 。这些事件按照以下算法处理。

  1. 一旦I/O请求被处理(文件中的数据、套接字中的数据可以读取等), Event Demultiplexer 将为这个特定的操作注册回掉并添加到 Event Queue
  2. Event Queue 中的事件可以被处理时,它们将按照接收到的顺序顺序执行,直到队列为空。
  3. 如果 Event Queue 中没有事件,或者 Event Demultiplexer 中没有任何挂起的请求,则程序将完成。否则,这个过程将又从第一步开始,如此循环。

上面的图是对NodeJS如何工作的高级概述,并显示了一个称为Reactor 模式的主要部分。但实际却这比这复杂得多。这有多复杂呢?

Event Demultiplexer 不是一个独立的部分,它讲包括所有操作系统平台以及所有类型的I/O。

这里显示的 Event Queue 也仅仅包含一个的队列,所有类型的事件都在其中排队进入和退出队列。并不是只有I/O类型的事件会在这里排队

所以接下来让我们更加深入的理解

Event Demultiplexer只是一个抽象概念,它并不真实存在。在不同的操作系统中,它都有实现,有着不同的名称。

IOCP 等。 Nodejs 使用它提供的 非阻塞、异步硬件I/O功能

令人困惑的是,并不是所有的I/O类型都能使用这个实现来执行,即使在同一个操作系统平台上,支持不同类型的I/O也很复杂。

例如Linux不支持文件系统访问的完全异步,为了提供完全的异步,要处理所有这些文件系统的复杂性是非常复杂的/几乎不可能的。

另外处理File I/O之外,node提供的DNS也具有这种复杂性

现在我们知道并不是所有的I/O函数都发生在 thread pool 中。NodeJS已经尽力使用 非阻塞和异步硬件I/O 来完成大部分I/O,但是对于阻塞或难以处理的I/O类型,它使用 thread pool

将前面提到的问题聚集到一起

正如我们所看到的,很难在所有不同类型的OS平台上支持所有不同类型的I/O((file I/O, network I/O, DNS等)。一些I/O可以使用本地硬件实现来执行,同时保留完整的异步性。而一些特定的I/O应该在线程池中执行某些I/O类型,以保证完整的异步性。

开发人员对Node的一个常见误解是Node在线程池中的执行所有类型的I/O。

为了在支持跨平台I/O的同时管理整个流程,应该有一个抽象层来封装这些平台间和平台内的复杂性,并为node的上层公开一个通用API。

接下来就让我们热烈欢迎........

这个库提供的不仅仅是对不同I/O轮询机制的简单抽象:‘handles’ 和 ‘streams’ 为套接字和其他实体提供了高级抽象;此外,还提供了跨平台的文件I/O和线程功能。

现在我们来看看libuv是如何组成的

事件队列中,所有事件都被事件循环按顺序排队和处理,直到队列为空。

在NodeJS中有多个队列,不同类型的事件在它们自己的队列中排队。

在处理一个类型的队列之后,在进入下一个队列之前,事件循环将处理两个 intermediate queues (中间队列),直到中间队列为空

那么有多少个队列呢?什么是中间队列?

有四种 主要的事件类型 ,它由本地的libuv处理。

这些不同类型的事件队列如何工作?

如下图所示,Node通过检查计时器队列中的是否有过期计时器从而开始事件循环,并在每个步骤中遍历每个队列。在处理了 close handlers queue 之后,如果任何队列中没有要处理的项,则循环将退出。可以将事件循环中每个队列的处理视为事件循环的一个阶段。

前面提到的中间队就是图中心得两份队列,有趣的是,一旦一个阶段完成,事件循环将检查这两个中间队列中的任何可用项。如果中间队列中有任何可用项,事件循环将立即开始处理它们,直到清空两个立即队列。直到它们为空,事件循环才会继续到下一个阶段。

两个中间队列的优先级不同, Next tick队列 具有比 其他微任务队列 更高的优先级。也就是说当一个阶段完成之后,会先去Next tick 队列清空任务,再去Microtasks(微任务)队列清空任务。

这些所谓的 中间队列 的约定引入了一个新问题,IO饥饿。大量使用 process.nextTick 将强制事件循环在不向前移动的情况下无限期地处理next tick队列。这将导致IO饥饿,因为如果不清空 Next tick 队列,事件循环将无法继续。

我将在后面的文章中以示例深入描述这些队列。

来一个总结,node在 Event Demultiplexer 中处理所有的异步I/O,它是 Libuv 抽象的I/O处理api集合,在I/O响应后,将相应的事件推入到相应的事件类型的队列,并在其中通过事件循环来调用回掉函数。

最后,现在您知道了什么是事件循环(即在event)、如何实现它以及Node如何处理异步I/O。现在让我们看看Libuv在NodeJS体系结构中的位置。

以上所述就是小编给大家介绍的《node端事件循环机制(Part1)》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 的支持!

本文通过实例给大家详细分析了JS中事件循环机制的原理和用法,以下是全部内容:

 

有其他语言能完成预期的功能吗?Java, 在Java.util.Timer中,对于定时任务的解决方案是通过多线程手段实现的,任务对象存储在任务队列,由专门的调度线程,在新的子线程中完成任务的执行

JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。

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

JS执行时会形成调用栈,调用一个函数时,返回地址、参数、本地变量都会被推入栈中,如果当前正在运行的函数中调用另外一个函数,则该函数相关内容也会被推入栈顶.该函数执行完毕,则会被弹出调用栈.变量也随之弹出,由于复杂类型值存放于堆中,因此弹出的只是指针,他们的值依然在堆中,由GC决定回收.

JavaScript 主线程拥有一个执行栈以及一个任务队列

遇到异步操作(例如:setTimeout, AJAX)时,异步操作会由浏览器(OS)执行,浏览器会在这些任务完成后,将事先定义的回调函数推入主线程的任务队列(task queue)中,当主线程的执行栈清空之后会读取task queue中的回调函数,当task queue被读取完毕之后,主线程接着执行,从而进入一个无限的循环,这就是事件循环.

主线程执行栈 & 任务队列 循环执行,构成事件循环

setTimeout()只是将事件插入了"任务队列",必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。要是当前代码耗时很长,有可能要等很久,所以并没有办法保证,回调函数一定会在setTimeout()指定的时间执行。

 

macrotask 和 microtask 是异步任务的两种分类。在挂起任务时,JS 引擎会将所有任务按照类别分到这两个队列中,首先在 macrotask 的队列(这个队列也被叫做 task queue)中取出第一个任务,执行完毕后取出 microtask 队列中的所有任务顺序执行;之后再取 macrotask 任务,周而复始,直至两个队列的任务都取完。

process.nextTick指定的任务总是发生在所有异步任务之前

 
 

我要回帖

更多关于 js宏观事件 的文章

 

随机推荐