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


结束语

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


目录
相关文章
|
4月前
|
Web App开发 JavaScript 前端开发
Node.js 是一种基于 Chrome V8 引擎的后端开发技术,以其高效、灵活著称。本文将介绍 Node.js 的基础概念
Node.js 是一种基于 Chrome V8 引擎的后端开发技术,以其高效、灵活著称。本文将介绍 Node.js 的基础概念,包括事件驱动、单线程模型和模块系统;探讨其安装配置、核心模块使用、实战应用如搭建 Web 服务器、文件操作及实时通信;分析项目结构与开发流程,讨论其优势与挑战,并通过案例展示 Node.js 在实际项目中的应用,旨在帮助开发者更好地掌握这一强大工具。
89 1
|
2月前
|
Web App开发 前端开发 JavaScript
折腾之王:JavaScript之父Brave浏览器与BAT的诞生
2015年,JavaScript之父Brendan Eich再次创业,推出Brave浏览器和加密货币Basic Attention Token(BAT),旨在颠覆传统广告行业。Brave屏蔽广告、保护隐私,加载速度快;BAT则通过奖励机制让用户、内容创作者和广告主三方受益。尽管面临用户习惯和巨头竞争的挑战,Brave已拥有超4000万月活跃用户,成为全球增长最快的隐私浏览器,引领Web3生态发展。
129 22
折腾之王:JavaScript之父Brave浏览器与BAT的诞生
|
4月前
|
JavaScript 前端开发 数据处理
模板字符串和普通字符串在浏览器和 Node.js 中的性能表现是否一致?
综上所述,模板字符串和普通字符串在浏览器和 Node.js 中的性能表现既有相似之处,也有不同之处。在实际应用中,需要根据具体的场景和性能需求来选择使用哪种字符串处理方式,以达到最佳的性能和开发效率。
130 63
|
4月前
|
移动开发 JavaScript 前端开发
一些处理浏览器兼容性问题的JavaScript库
这些库在处理浏览器兼容性问题方面都有着各自的特点和优势,可以根据具体的需求和项目情况选择合适的库来使用,从而提高代码的兼容性和稳定性,为用户提供更好的体验。同时,随着浏览器技术的不断发展,还需要持续关注和学习新的兼容性解决方案。
216 58
|
4月前
|
算法 开发者
Moment.js库是如何处理不同浏览器的时间戳格式差异的?
总的来说,Moment.js 通过一系列的技术手段和策略,有效地处理了不同浏览器的时间戳格式差异,为开发者提供了一个稳定、可靠且易于使用的时间处理工具。
150 57
|
3月前
|
存储 JavaScript NoSQL
Node.js新作《循序渐进Node.js企业级开发实践》简介
《循序渐进Node.js企业级开发实践》由清华大学出版社出版,基于Node.js 22.3.0编写,包含26个实战案例和43个上机练习,旨在帮助读者从基础到进阶全面掌握Node.js技术,适用于初学者、进阶开发者及全栈工程师。
87 9
|
4月前
|
JSON 移动开发 JavaScript
在浏览器执行js脚本的两种方式
【10月更文挑战第20天】本文介绍了在浏览器中执行HTTP请求的两种方式:`fetch`和`XMLHttpRequest`。`fetch`支持GET和POST请求,返回Promise对象,可以方便地处理异步操作。`XMLHttpRequest`则通过回调函数处理请求结果,适用于需要兼容旧浏览器的场景。文中还提供了具体的代码示例。
在浏览器执行js脚本的两种方式
|
4月前
|
JavaScript 前端开发 API
深入理解Node.js事件循环及其在后端开发中的应用
本文旨在揭示Node.js的核心特性之一——事件循环,并探讨其对后端开发实践的深远影响。通过剖析事件循环的工作原理和关键组件,我们不仅能够更好地理解Node.js的非阻塞I/O模型,还能学会如何优化我们的后端应用以提高性能和响应能力。文章将结合实例分析事件循环在处理大量并发请求时的优势,以及如何避免常见的编程陷阱,从而为读者提供从理论到实践的全面指导。
|
4月前
|
JSON JavaScript 前端开发
使用JavaScript和Node.js构建简单的RESTful API
使用JavaScript和Node.js构建简单的RESTful API

热门文章

最新文章