JavaScript 事件循环:理解进程、线程和异步编程

JavaScript 是一门单线程的编程语言,这意味着它只有一个主执行线程来处理所有的任务。然而,JavaScript 可以利用异步编程的方式实现并发操作,从而提高性能和用户体验。为了更好地理解 JavaScript 中的事件循环,我们首先要了解进程、线程和异步编程的概念。

进程与线程

进程是指操作系统中运行的一个程序实例。在计算机上同时运行着多个进程,每个进程都有自己的独立内存空间和执行环境。进程之间相互独立,彼此不会干扰。而线程是进程中更小的单位,是操作系统调度的基本单位。一个进程可以包含多个线程,这些线程共享进程的内存空间和其他资源。

简单来说,进程指的是cpu在运行指令和保存上下文所需的时间,线程指的是进程中更小的单位,指的是一段指令执行所需要的时间。

在浏览器中,每个标签页通常都是一个独立的进程,这样可以隔离不同页面的运行环境,提高安全性和稳定性。当我们在浏览器中新开一个标签页时,就会创建一个新的进程来处理该标签页的内容。而在这个进程中,又可以有多个线程来并行处理不同的任务。

举个例子,当我们在浏览器中新开一个标签页并访问一个网站时,这个进程中可能会有以下几个线程同时工作:

  1. 渲染线程:负责将 HTML、CSS 和 JavaScript 转换为可视化页面,处理页面的渲染和绘制。
  2. HTTP 请求线程:负责发送网络请求和接收响应,处理与服务器的通信。
  3. JavaScript 引擎线程:负责解析 JavaScript 代码并执行,处理页面的交互和动态效果。

需要知道的是,线程之间是可以一起工作的,这些线程在进程中互相协作,完成各自的任务。但是,渲染线程和 JavaScript 引擎线程是互斥的,即同一时间只能有一个线程在执行。这是因为渲染线程需要访问 DOM 树和样式表来进行页面渲染,而 JavaScript 引擎线程可能会修改 DOM 树或样式表,所以需要互斥执行,以免出现冲突。

单线程的JavaScript

由于 JavaScript 是单线程的,它在执行时只能按照顺序逐条执行代码。这样做有两个优点:

  1. 节约内存开销:由于只需要一个线程来执行代码,不需要为每个线程分配独立的内存空间,从而节约了内存开销。
  2. 没有锁的概念:在多线程编程中,由于多个线程可能同时访问共享资源,需要引入锁机制来保证数据的一致性,但这会增加上下文切换的时间。而 JavaScript 的单线程模型不存在这个问题,可以避免锁的开销。

异步编程

JavaScript 通过异步编程的方式来实现并发操作。它将任务分为宏任务和微任务两种类型。

宏任务(Macrotask)包括以下几种:

  • script:整体的 JavaScript 代码块。
  • setTimeout 和 setInterval:定时器任务。
  • setImmediate:在当前事件循环完成后立即执行的任务。
  • I/O 操作:例如网络请求、文件读写等。
  • UI 渲染:浏览器需要绘制页面时触发的任务。

微任务(Microtask)包括以下几种:

  • Promise.then():Promise 的回调函数。
  • MutationObserver:DOM 变动观察器。
  • process.nextTick():Node.js 中的微任务。

事件循环(event-loop)

事件循环(Event Loop)是 JavaScript 实现异步编程的关键机制。它负责监听、收集和执行宏任务和微任务。事件循环的执行过程如下:

  1. 执行同步代码(宏任务):首先执行当前执行上下文中的同步代码,按照顺序逐条执行。
  2. 查询是否有异步任务需要执行:当执行栈为空时,事件循环开始查询是否有需要执行的异步任务。
  3. 执行微任务(优先级高):如果有微任务,事件循环会依次执行微任务队列中的任务,直到队列为空。
  4. 如果有需要,渲染页面:如果需要更新页面,浏览器会在此阶段进行页面的渲染,保证用户界面的及时响应。
  5. 执行宏任务(下一次事件循环的开始):事件循环会从宏任务队列中取出一个任务执行,然后回到第一步,继续循环执行。

我们来看一个例子:

scss 复制代码
console.log(1);

setTimeout(() => {
  console.log(2);
  new Promise((resolve) => {
    console.log(4);
    resolve()
    setTimeout(() => {
      console.log(6);
    }) 
  }).then(() => {
    console.log(5);`
  })
}, 1000)

console.log(3);

这段代码最终的输出顺序是什么样的呢?

第一步执行同步代码,看这段代码首先是一个console.log(1);之后是一个异步的定时器setTimeout(),它是宏任务,我们将它加入宏任务队列[ setTimeout() ],再就是console.log(3);所以执行同步代码输出的是1,3

第二步查询是否有异步任务需要执行,此时宏任务队列里有一个定时器,微任务队列为空,第三步和第四步不用做。

到第五步执行宏任务,执行宏任务队列里的定时器。我们再来看setTimeout()里的代码,回到第一步重新开始。

执行同步代码 ,首先打印2,此时的输出顺序是1,3,2,之后是一个new Promise(),它是立即执行的,于是执行它里面的代码打印了4,输出顺序为1,3,2,4,之后是一个resolve的调用,再就是一个定时器setTimeout(),我们又把它加入到宏任务队列[ setTimeout() setTimeout() ],宏任务队列里的第一个定时器我们已经开始执行了,所以把它划掉。new Promise()之后还有个.then(),这个是微任务,于是把它加入微任务队列里[ Promise.then() ]。此时宏任务和微任务队列不为空,查询到有异步任务 ,于是开始执行微任务 队列里的Promise.then(),输出5,此时输出顺序为1,3,2,4,5,然后执行宏任务 队列里的定时器,里面是立即执行的console.log(6),此时全部代码都执行完毕,最终的输出顺序为1,3,2,4,5,6

注意await

这里我们还要再讲一下await会造成的影响,当代码中有await时,浏览器会给await开小灶提速,让它立即执行,而把它之后的代码挤入微任务队列。

我们来看例子,顺便练习一下刚刚学的知识:

javascript 复制代码
console.log('start');
async function async1() {
  await async2() // 浏览器给await开小灶提速
  console.log('async1'); // 被await挤入微任务队列
}
async function async2() {
  console.log('async2');
}
async1()
setTimeout(function() {
  console.log('setTimeout');
}, 0)
new Promise((resolve) => {
  console.log('promise');
  resolve()
})
.then(() => {  
  console.log('then1');
})
.then(() => {
  console.log('then2');
})
console.log('end');

看完这段代码你得出的输出顺序是什么呢?

正确答案是start,async2,promise,end,async1,then1,then2,setTimeout,你答对了吗?

我们来看下过程,第一步执行同步代码,首先是console.log('start');,输出了start,然后是async1(),于是我们去执行函数async1,里面是await async2()于是立即执行调用async2,函数async2里面是console.log('async2'); 于是现在的输出是start,async2。await async2()之后的代码是 console.log('async1');它被挤入了微任务队列[ console.log('async1') ]。此时async1的调用完成,之后是一个定时器,把它放入宏任务队列[ setTimeout() ],之后是立即执行的new Promise(),里面是console.log('promise');,此时输出为start,async2,promise。后面有两个.then(),把它们加入微任务队列[ console.log('async1'),.then(),.then()],最后是一个打印end,此时输出为start,async2,promise,end。然后是执行异步任务,先是微任务,一个个执行微任务队列里的微任务,此时输出为start,async2,promise,end,async1,then1,then2。然后执行宏任务,就一个setTimeout()输出'setTimeout',所以最终的输出就是start,async2,promise,end,async1,then1,then2,setTimeout啦,你学会了吗?

今天的内容到这就结束啦,欢迎下次再来一起学习ヾ(◍°∇°◍)ノ゙!!

相关推荐
小飞猪Jay27 分钟前
C++面试速通宝典——13
jvm·c++·面试
_.Switch41 分钟前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
一路向前的月光1 小时前
Vue2中的监听和计算属性的区别
前端·javascript·vue.js
长路 ㅤ   1 小时前
vite学习教程06、vite.config.js配置
前端·vite配置·端口设置·本地开发
长路 ㅤ   1 小时前
vue-live2d看板娘集成方案设计使用教程
前端·javascript·vue.js·live2d
Fan_web1 小时前
jQuery——事件委托
开发语言·前端·javascript·css·jquery
安冬的码畜日常1 小时前
【CSS in Depth 2 精译_044】第七章 响应式设计概述
前端·css·css3·html5·响应式设计·响应式
莹雨潇潇2 小时前
Docker 快速入门(Ubuntu版)
java·前端·docker·容器
Jiaberrr2 小时前
Element UI教程:如何将Radio单选框的圆框改为方框
前端·javascript·vue.js·ui·elementui
Tiffany_Ho3 小时前
【TypeScript】知识点梳理(三)
前端·typescript