JavaScript 事件循环(EventLoop) —— 浏览器 & Node

简介: JavaScript 事件循环(EventLoop) —— 浏览器 & Node

image.png


一、事件循环的本质

本质:运行时对 JS 脚本的调度方式就叫做事件循环.

  • 对于 浏览器 而言,需要考虑用户交互、UI渲染、脚本运行、网络请求等操作,这些操作必然都依赖于事件去执行,因此,为了协调事件必须要使用事件循环.
  • 对于 Node 而言,尽管 JavaScript 是单线程的,但系统内核是多线程的,它们可以在后台处理多种操作,当其中一个完成操作的时候,内核将通知 Node 将适合的回调添加到队列中,并等待时机执行. 而事件循环是 Node 处理非阻塞 I/O 操作的机制.

二、浏览器的事件循环

JS 为什么是单线程?

JavaScript 的用途决定了它的单线程。

作为浏览器脚本语言,JavaScript 的主要用途是与用户互动,以及操作 DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定 JavaScript 同时有两个线程,一个线程在某个 DOM 节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

任务队列

产生任务队列的原因?

单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。

如果排队是因为计算量大,CPU 处理不过来,这时候也算合理,但很多时候 CPU 是空闲的,是因为 IO 设备(输入输出设备)很慢(比如 Ajax 操作从网络读取数据),CPU 不得不等着结果返回,才能继续往下执行。

JavaScript 语言的设计者意识到,这时主线程完全可以不管 IO 设备,挂起处于等待中的任务,先运行排在后面的任务。等到 IO 设备返回了结果,再回过头,把挂起的任务继续执行下去。

任务队列类型

所有任务可以分成两种,一种是 同步任务(synchronous),另一种是 异步任务(asynchronous)。

同步任务 指的是 在主线程上排队执行的任务 ,只有前一个任务执行完毕,才能执行后一个任务;

异步任务 指的是 不进入主线程,而进入"任务队列"(task queue)的任务,只有 "任务队列" 通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

异步执行的运行机制(同步执行也是如此,因为它可以被视为没有异步任务的异步执行)

(1)所有同步任务都在 主线程 上执行,形成一个 执行栈(execution context stack)。

(2)主线程之外,还存在一个 "任务队列"(task queue)。只要 异步任务 有了运行结果,就在 "任务队列" 之中放置一个事件。

(3)一旦 "执行栈" 中的 所有同步任务执行完毕,系统就会 读取 "任务队列" ,看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。

(4)主线程不断重复上面的第三步。

主线程 "任务队列" 中读取事件,这个过程是循环不断的,所以整个的运行机制又被称为 事件循环(Event Loop)

image.png

image.png

宏任务和微任务

除了广义的定义 同步任务异步任务,JavaScript 单线程中的任务还可以细分为 宏任务(macrotask)微任务(microtask)

宏任务包含:

  • script (整体代码)
  • setTimeout
  • setInterval
  • I/O
  • UI 交互事件
  • postMessage
  • MessageChannel
  • setImmediate(Node.js)

注意: 很多人可能认为 requestAnimationFrame 是宏任务,但确切的说它的执行时机和宏任务还是有些不同的,具体可见

微任务包含:

  • Promise.then、catch 、finally
  • queueMicrotask()
  • MutationObserver
  • process.nextTick (Node.js)

image.png

通过题目进行分析

  • 一轮事件循环,会执行一次宏任务,以及本轮循环产生的所有微任务.
  • 任务队列保持先进先出的顺序去执行.
// 下面的输出顺序是什么?
setTimeout(() => {
  console.log('setTimeout');
}, 0);
Promise.resolve().then(()=>{
  console.log('promise');
});
console.log('main');
复制代码

相信上面的题目对大家来说都不困难,下面通过图片来展示相应的流程:

image.png

到现在你应该对整个浏览器事件循环有所理解,可以尝试分析下面这道题目:

  • PS:如果你仍旧不是很理解这个流程,建议按照本文中画流程图的形式去进行分析
setTimeout(() => {
  console.log('setTimeout_outer');
  Promise.resolve().then(() => {
    console.log('promise in setTimeout_outer');
  });
}, 0);
Promise.resolve().then(() => {
  console.log('promise_outer');
  setTimeout(() => {
    console.log('setTimeout in promise_outer');
  }, 0);
});
console.log('main');
复制代码

三、Node 中的事件循环

事件循环机制解析

当 Node.js 启动后,它会初始化事件循环,处理已提供的输入脚本,它可能会调用一些异步的 API、调度定时器,或者调用 process.nextTick(),然后开始处理事件循环。

每一个 Event Loop 都会包含如下顺序的六个阶段,它和浏览器的事件循环是完全不同的。

image.png

六个阶段

  • timers (定时器):本阶段执行已经被 setTimeout()setInterval() 的调度回调函数。
  • pending callbacks (待定回调):执行延迟到下一个循环迭代的 I/O 回调。
  • idle, prepare:仅系统内部使用。
  • poll (轮询):检索新的 I/O 事件;执行与 I/O 相关的回调(几乎所有情况下,除了关闭的回调函数,那些由计时器和 setImmediate() 调度的之外),其余情况 node 将在适当的时候在此阻塞。
  • check (检测)setImmediate() 回调函数在这里执行。
  • close callback (关闭的回调函数):一些关闭的回调函数,如:socket.on('close', ...)

其中 poll (轮询) 阶段有两个重要的功能:

  1. 计算应该阻塞和轮询 I/O 的时间。
  2. 然后,处理 轮询 队列里的事件。

当事件循环进入 轮询 阶段且 没有被调度的计时器时 ,将发生以下 两种情况之一

  • 如果 轮询 队列 不是空的 ,事件循环将循环访问回调队列并同步执行它们,直到队列已用尽,或者达到了与系统相关的硬性限制。
  • 如果 轮询 队列 是空的 ,还有两件事发生:
  • 如果脚本被 setImmediate() 调度,则事件循环将结束 轮询 阶段,并继续 检查 阶段以执行那些被调度的脚本。
  • 如果脚本 未被setImmediate()调度,则事件循环将等待回调被添加到队列中,然后立即执行。

一旦 poll(轮询) 队列为空,事件循环将检查 已达到时间阈值的计时器。如果一个或多个计时器已准备就绪,则事件循环将绕回timers(计时器) 阶段以执行这些计时器的回调。

setImmediate() 对比 setTimeout()

setImmediate()setTimeout() 很类似,但它们被调用的时机不同。

  • setImmediate() 设计为一旦在当前 轮询 阶段完成, 就执行脚本.
  • setTimeout() 在最小阈值(ms 单位)过后运行脚本.
  • setImmediate() 相对于 setTimeout() 优势在于,如果 setImmediate() 是在  I/O 周期内 被调度的,那它将会在其中 任何的定时器之前执行,跟存在多少个定时器无关.

执行计时器的顺序将根据调用它们的上下文而异

如果二者都从主模块内调用,则计时器将受进程性能的约束(这可能会受到计算机上其他正在运行应用程序的影响)。

例如,如果运行以下 不在 I/O 周期(即主模块)内的脚本 ,则执行两个计时器的顺序是 非确定性 的,因为它受进程性能的约束,或者说受 event loop 启动时间的影响:

// timeout_vs_immediate.js
setTimeout(() => {
  console.log('timeout');
}, 0);
setImmediate(() => {
  console.log('immediate');
});
输出的结果可能为:timeout  immediate 或者 immediate  timeout
复制代码

下面给出了存在上述输出结果的两种情况 图解

image.png

如果把这两个函数放入一个 I/O 循环 内调用,setImmediate 总是被 优先调用

// timeout_vs_immediate.js
const fs = require('fs');
fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('timeout');
  }, 0);
  setImmediate(() => {
    console.log('immediate');
  });
});
输出结果恒为:immediate  timeout
复制代码

同一个 I/O 循环中更有确定性,因此,你也可以自己尝试画流程图的方式去理解它.

process.nextTick()

  • process.nextTick() 是异步 API 的一部分,它并不是事件循环的一部分.
  • process.nextTick() 始终在当前操作完成后处理 nextTickQueue,而不管事件循环的当前阶段如何.
  • 任何时候在给定的阶段中调用的 process.nextTick(),不在六个阶段中的任一阶段内执行,而是在 一个阶段切换到下一个阶段之前执行.

process.nextTick() 对比 setImmediate()

  • process.nextTick() 在同一个阶段立即执行.
  • setImmediate() 在事件循环的接下来的迭代或 'tick' 上触发.
  • process.nextTick()setImmediate() 触发得更快. 下面通过具体的例子,来验证三者的关系:
const fs = require('fs');
const path = require('path');
fs.readFile(path.resolve(__dirname, '/index.html'), () => {
  setTimeout(() => {
    console.log('setTimeout');
    process.nextTick(() => {
      console.log('nextTick in setTimeout');
    });
  }, 1);
  setImmediate(() => {
    console.log('setImmediate');
    process.nextTick(() => {
      console.log('nextTick in setImmediate');
    });
  });
  process.nextTick(() => {
    console.log('nextTick outer'); 
  });
});
复制代码

下面给出流程分析图解:

image.png


结束语

事件循环的相关内容相对来说还是迂回环绕的,并不是任何一篇文章就能够了解透彻,里面的很多执行流程,最好自己画图去辅助理解,不要凭空想象.


目录
相关文章
|
1月前
|
存储 JavaScript 前端开发
深入理解JavaScript中的事件循环(Event Loop):机制与实现
【10月更文挑战第12天】深入理解JavaScript中的事件循环(Event Loop):机制与实现
75 3
|
14天前
|
JSON 移动开发 JavaScript
在浏览器执行js脚本的两种方式
【10月更文挑战第20天】本文介绍了在浏览器中执行HTTP请求的两种方式:`fetch`和`XMLHttpRequest`。`fetch`支持GET和POST请求,返回Promise对象,可以方便地处理异步操作。`XMLHttpRequest`则通过回调函数处理请求结果,适用于需要兼容旧浏览器的场景。文中还提供了具体的代码示例。
在浏览器执行js脚本的两种方式
|
11天前
|
存储 JavaScript 网络协议
浏览器与 Node 的事件循环
浏览器和Node.js的事件循环是异步操作的核心机制。它们通过管理任务队列和回调函数,确保程序在处理耗时任务时不会阻塞主线程,从而实现高效、响应式的应用开发。
|
11天前
|
机器学习/深度学习 自然语言处理 前端开发
前端神经网络入门:Brain.js - 详细介绍和对比不同的实现 - CNN、RNN、DNN、FFNN -无需准备环境打开浏览器即可测试运行-支持WebGPU加速
本文介绍了如何使用 JavaScript 神经网络库 **Brain.js** 实现不同类型的神经网络,包括前馈神经网络(FFNN)、深度神经网络(DNN)和循环神经网络(RNN)。通过简单的示例和代码,帮助前端开发者快速入门并理解神经网络的基本概念。文章还对比了各类神经网络的特点和适用场景,并简要介绍了卷积神经网络(CNN)的替代方案。
|
18天前
|
Web App开发 JavaScript 前端开发
使用 Chrome 浏览器的内存分析工具来检测 JavaScript 中的内存泄漏
【10月更文挑战第25天】利用 Chrome 浏览器的内存分析工具,可以较为准确地检测 JavaScript 中的内存泄漏问题,并帮助我们找出潜在的泄漏点,以便采取相应的解决措施。
127 9
|
12天前
|
JavaScript 前端开发 中间件
JS服务端技术—Node.js知识点
本文介绍了Node.js中的几个重要模块,包括NPM、Buffer、fs模块、path模块、express模块、http模块以及mysql模块。每部分不仅提供了基础概念,还推荐了相关博文供深入学习。特别强调了express模块的使用,包括响应相关函数、中间件、Router和请求体数据解析等内容。文章还讨论了静态资源无法访问的问题及其解决方案,并总结了一些通用设置。适合Node.js初学者参考学习。
29 1
|
17天前
|
开发框架 JavaScript 前端开发
Node.js日记:客户端和服务端介绍、Node.js介绍
Node.js日记:客户端和服务端介绍、Node.js介绍
|
16天前
|
JavaScript 前端开发 开发者
JavaScript的事件循环
【10月更文挑战第27天】理解JavaScript的事件循环机制对于正确编写和理解JavaScript中的异步代码至关重要,它是JavaScript能够高效处理各种异步任务的关键所在。
29 1
|
21天前
|
JavaScript 前端开发 开发工具
Node.js——初识Node.js
Node.js——初识Node.js
20 4