JS事件循环机制(Event Loop)

引入

事件循环是JavaScript执行上下文中的一种机制,用于处理异步操作。它的核心思想是将所有的异步任务放入一个队列中,然后按照队列中的顺序依次执行,直到队列为空为止。主线程从任务队列中读取事件,这个过程是循环不断的,所以整个的这种运行机制称为事件循环(Event Loop) 。下文将一步步引导,让您明白这JS事件循环机制!

进程和线程

进程:进程是一个内存中正在运行的应用程序,是cpu资源分配的最小单位。每个进程都有自己独立的内存空间和执行环境,可以同时运行多个进程,例如:电脑中打开的任务管理器里面可以看到一个个进程,如下图:

线程:线程是cpu调度的最小单位,它是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程。线程共享进程的内存空间和资源,但是每个线程有自己的栈空间和程序计数器。

以音乐播放软件为例,一个进程可能负责整个软件的运行,而其中的线程则分别负责播放音乐、下载音乐、处理用户界面等不同的任务。这些线程在进程的管理下,协同工作,共同完成音乐播放软件的功能。

单线程:同一个时间只能运行一个任务,就意味着所有的任务都需要排队,前一个任务结束,才会执行后一个任务

:是一种后进先出的数据结构,数据元素在插入(即进栈)和删除(即出栈)时均从栈顶进行操作。

队列:是一种先进先出的数据结构,数据元素在队尾插入,从队首删除的。

任务队列:是一个事件的队列,只要指定过回调函数,这些事件发生时就会进入任务队列,等待主线程读取。主线程读取任务队列,就是读取里面有哪些事件。

回调函数(callback):就是那些会被主线程挂起来的代码,异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。

JS是单线程语言

原因

JS作为浏览器脚本语言,主要是用于与用户互动以及操作DOM。这就决定了JS只能是单线程的,否则会带来很多复杂的同步问题。比如JS有两个线程,一个线程在DOM节点上添加内容,一个线程在DOM节点上删除内容,那浏览器应该执行哪一个线程的任务呢?

问题

单线程只能一个个排着队执行,当执行以下代码会导致阻塞:while死循环,不会弹出hello

while(1){} alert('hello');

解决

上述出现的问题,可知:如果前一个任务耗时很长,后一个任务就得一直等着,这样就会造成线程堵塞。

这时javascript开发人员意识到,为了不影响主线程正常运行,就把那些耗时的任务挂起来,依次放进一个任务队列,等主线程的任务执行完毕后,再回来去继续执行队列中的任务。

同步任务和异步任务

按照以上的方法,可以将执行任务可以分为两种:

  • 同步任务:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。
  • 异步任务:不进入主线程,进入任务队列的任务,只有任务队列通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
    • 宏任务:需要相对较长时间才能完成的任务,如:setTimeout回调函数setInterval回调函数I/OAJAXDOM事件
    • 微任务:较短时间内完成的任务,如:Promise.then(回调)async/awaitnextTick
  • :在JavaScript中,当创建一个新的Promise时,传递一个函数(称为executor函数)给Promise构造函数。这个executor函数会立即同步执行,并且在这个函数内部执行的任何代码都是同步的。

补充

  • 当宏任务和微任务都处于任务队列中时,微任务的优先级大于宏任务,即先将微任务执行完,再执行宏任务;
  • 当执行一个宏任务时,如果宏任务中产生了新的微任务,这些微任务不会立刻执行,而是会被放入到当前微任务队列中,在当前宏任务执行完毕后被依次执行。
  • 执行微任务过程中产生的新的微任务并不会推迟到下一个循环中执行,而是在当前的循环中继续执行

JS事件循环机制

事件循环的工作原理步骤大致如下:

1.先执行同步代码,所有同步代码都在主线程上执行,形成一个执行栈。

2.当遇到异步任务时,会将其挂起并添加到任务队列中,宏任务放入宏任务队列,微任务放进微任务队列,只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。

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

4.重复上述步骤,直到任务队列为空

:主线程的读取过程基本上是自动的,只要执行栈一清空,"任务队列"上第一位的事件就自动进入主线程。但是,由于存在后文提到的"定时器"功能,主线程首先要检查一下执行时间,某些事件只有到了规定的时间,才能返回主线程。

练习

输出1

js 复制代码
console.log('1')

setTimeout(function (){
	console.log('2')
}, 1000)

console.log('3')

输出结果:1 3 2

setTimeout回调进入异步任务中的宏任务,从上到下先执行同步任务,再执行宏任务。

输出2

js 复制代码
console.log('1')

setTimeout(function callback(){
	console.log('2')
}, 1000)

new Promise((resolve, reject) => {
    console.log('3')
    resolve()
})
.then(res => {
    console.log('4');
})

console.log('5')

输出结果:1 3 5 4 2

创建一个新的Promise时,传递的executor函数Promise构造函数。这个executor函数会立即同步执行,并且在这个函数内部执行的任何代码(如console.log('3'))都是同步的。

输出3

js 复制代码
setTimeout( () => {
  console.log('1')
  Promise.resolve().then( () => {
    console.log('2')
  })
},0)

new Promise((resolve, reject) => {
    console.log('3')
    resolve()
})
.then(res => {
    console.log('4');
})

console.log('5')

输出结果:3 5 4 1 2

解析: (1)首先遇到定时器,将定时器的回调函数放入任务队列中的宏任务,其中宏任务中包括同步任务console.log('1')微任务then回调函数

(2)创建一个新的Promise,传递的executor函数给Promise构造函数。executor函数立即同步执行,即console.log('3')是同步的,即先输出3,再将then回调函数放入微任务队列

(3)执行同步代码,输出5

(4)主线程执行栈为空,微任务列和宏任务列非空,执行微任务列,输出4

(5)微任务为空,执行宏任务列,先输出同步任务1,再输出微任务2

输出4

js 复制代码
 setTimeout(function(){
     console.log('定时器setTimeout')
 },0);
 
 new Promise(function(resolve){
     console.log('同步任务1');
     for(var i = 0; i < 10000; i++){
         i == 99 && resolve();
     }
 }).then(function(){
     console.log('Promise.then')
 });
 
 this.$nextTick(() => {
    console.log('nextTick')
 })

 console.log('同步任务2');
 

输出结果: 同步任务1 => 同步任务2 => nextTick => Promise.then => 定时器setTimeout

提示及注意

(1)遇到 nextTick,把它放到 微任务队列 中。在同步代码执行完成后,不管其他异步任务,优先执行 nextTick。

因为nextTick函数是 Node.js 环境提供的一个特殊函数, nextTick 函数中的回调函数将在当前事件循环中的其他微任务之前执行。所以由于触发时机不同,nextTick函数中的微任务会优先其它微任务执行

(2)在浏览器中 setTimeout 的延时设置为 0 的话,会默认为 4ms,NodeJS 为 1ms;具体值可能不固定,但不是为 0。

输出5

js 复制代码
for (var i = 0; i < 5; i++) {
    setTimeout(function() {
      console.log('异步任务', i);
    }, 1000);
}

console.log('同步任务', i);

输出结果:第一个5直接输出,1秒后,输出5 5 5 5 5

解析

(1)当 i = 0,1,2,3,4都满足条件,进入循环,遇到setTimeout是异步操作,依次将定时器的回调函数放入任务队列的宏任务;

(2)当 i = 5 时,不满足条件,for循环结束。 console.log('同步任务', i)入栈,此时i值被累加到了5。输出5;

(3)1s过去后,主线程去执行任务队列中的函数,5个function依次入栈执行之后再出栈,此时的i已经变成了5。因此几乎同时输出5个5;

因此输出结果就是一个5直接输出,1s之后,输出5个5。

如果想要的结果是 5 0 1 2 3 4,需要再改下代码:上面的代码使用var定义变量,当定时器触发时,for循环已经完成,变量i的值已经变成了5,为了解决这个问题,可以使用块级作用域来捕获每次循环的值。

js 复制代码
for (let i = 0; i < 5; i++) {
    setTimeout(function() {
      console.log('异步任务', i);
    }, 1000);
}

console.log('同步任务', i);

输出结果:第一个5直接输出,1秒后,输出0 1 2 3 4

将 var 改为 let 后,我们就可以得到期望的输出结果 0、1、2、3、4。

这是因为 let 声明的变量具有块级作用域,每次迭代都会创建一个新的 i 变量,并且在每个 setTimeout 回调函数内部,都能访问到对应的 i 变量。因此,改用 let 关键字声明 i,可以有效避免由闭包引起的变量共享问题。

输出6

js 复制代码
async function async1() {
  console.log("async1 start");
  await async2();
  console.log("async1 end");
}
async function async2() {
  console.log("async2 start");
  return new Promise((resolve, reject) => {
    resolve();
    console.log("async promise");
  });
}
console.log("script start");
async1();
setTimeout(() => {
  console.log("setTimeout");
}, 0);
new Promise((resolve, reject) => {
  console.log("promise1");
  resolve();
})
  .then(() => {
    console.log("promise2");
  })
  .then(() => {
    console.log("promise3");
  });
console.log("script end");

输出结果script start => async1 start => async2 start => async promise => promise1 => script end => promise2 => promise3 => async1 end => setTimeout

  • async/await函数是为了简化多个then链情况而出现的

  • async关键字用于声明一个函数是异步的,搭配await实现函数的异步执行,使用async/await时要等await后面的返回值之后才能继续往下执行在async函数中,如果没有await关键字,则函数与普通函数没有什么区别,函数代码将会同步执行

  • 含有await的async中,同一代码块中await后的所有代码将被放置返回的promise的then方法中执行,即await后的代码被加载进微任务队列

  • 当函数内部执行到一个await语句的时候,如果语句返回一个 promise 对象,那么函数将会等待promise 对象的状态变为 resolve 后再继续向下执行

解析

(1)async关键字声明两个异步函数async1和async2

(2)执行console.log("script start"); ,输出 "script start"

(3)调用 async1(),开始执行异步函数async1console.log("async1 start");,输出 "async1 start"

(4)调用 await async2(),async1 暂停执行,等待async2Promise解决

(5)async2 被调用,输出" async2 start ",async2 创建一个新的 Promise 并立即执行其 executor 函数,在 Promise 的 executor 函数中,resolve() 被调用,同时,console.log("async promise"); 同步执行,输出 "async promise"。

(6)new Promise((resolve, reject) => { ... }) 创建了一个新的 Promise,其 executor 函数立即执行,输出 "promise1"。

(7)console.log("script end"); 同步执行,输出 "script end"。

执行栈为空,处理微任务队列:

(8).then(() => { console.log("promise2"); }) 被执行,输出 "promise2"。

(9).then(() => { console.log("promise3"); }) 被执行,输出 "promise3"。

微任务队列为空,处理宏任务队列:

(10)setTimeout 的回调函数执行,输出 "setTimeout"。

(11)回到 async2,因为 resolve() 已经被调用,async1 会继续执行。

(12)console.log("async1 end");async1 中同步执行,输出 "async1 end"。

最后

JavaScript 是一门单线程语言,异步操作都是放到事件循环队列里面,等待主执行栈来执行的,并没有专门的异步执行线程。

拓展:

浏览器是多进程的

浏览器是多进程的,也是多线程的。假如假如,浏览器是单进程的,那么某个Tab页崩了或者某个插件崩了,都会影响整个浏览器,这样用户的体验感会完全差到爆炸滴!

浏览器每一个tab页都代表一个独立的浏览器进程,浏览器可能有自己的优化机制,有时候打开多个tab页后,可以在任务管理器中看到,有些进程被合并了,如下图的子帧,所以每一个Tab标签对应一个进程并不一定是绝对的。

--->浏览器设置--->更多工具--->任务管理器--->查看浏览器具体的进程内容

浏览器内核(浏览器渲染进程)属于浏览器多进程中的一种,主要负责页面渲染,脚本执行,事件处理等,在内核控制下各线程相互配合以保持同步,一个浏览器通常由以下常驻线程组成:

其中JS引擎线程就是执行javascript脚本程序的地方,JS引擎是浏览器的重要组成部分,主要是用于读取并执行JS。

JS引擎是多线程的,JS引擎解析但不仅限于javascript,单线程是指JS引擎执行JS时只分了一个线程给它执行,意思是JS引擎分配了一个线程给JavaScript执行,也就是我们所说的单线程。

各大浏览器的JS引擎

浏览器 Js引擎
Chrome V8
Firefox SpiderMonkey
IE Chakra
Safari Nitro/JavaScript Core
Opera Carakan

虽然各大浏览器的JS引擎都不同,但其执行机制大致相同。

相关推荐
ssshooter18 分钟前
VSCode 自带的 TS 版本可能跟项目TS 版本不一样
前端·面试·typescript
你的人类朋友23 分钟前
【Node.js】什么是Node.js
javascript·后端·node.js
Jerry1 小时前
Jetpack Compose 中的状态
前端
dae bal2 小时前
关于RSA和AES加密
前端·vue.js
柳杉2 小时前
使用three.js搭建3d隧道监测-2
前端·javascript·数据可视化
倔强青铜三2 小时前
苦练Python第39天:海象操作符 := 的入门、实战与避坑指南
人工智能·python·面试
lynn8570_blog2 小时前
低端设备加载webp ANR
前端·算法
LKAI.2 小时前
传统方式部署(RuoYi-Cloud)微服务
java·linux·前端·后端·微服务·node.js·ruoyi
刺客-Andy3 小时前
React 第七十节 Router中matchRoutes的使用详解及注意事项
前端·javascript·react.js
前端工作日常3 小时前
我对eslint的进一步学习
前端·eslint