深入理解JavaScript中的"事件循环(Event loop)"

105 阅读4分钟

前言

不知道大家在看到下面这段代码的时候会想到输出是什么

setTimeout(()=>{
  console.log(1)
},0)

new Promise((res)=>{
  res(2)
}).then((data)=>{
  console.log(data);
})

console.log(3)

正确答案是:3 → 2 → 1。但如果你答错了,别慌!今天我们就来揭秘这道题背后的核心机制—事件循环

进程与线程: 浏览器的交响乐

**事件循环(Event loop) 也称为消息循环。Javascript是一门单线程语言,而事件循环就是Javascript运行中用于处理异步任务的编程模型。

首先我们先来简单了解一下什么是进程和线程。

进程是指在一块独立的内存中运行的程序

线程是指程序中负责完成任务的单元,一个进程通常有多个线程。

我们可以在浏览器中点击更多任务,找到浏览器任务管理,在这里面可以看到浏览器的所有进程。

image.png 在浏览器中存在多个进程,其中每一个页面都是一个渲染进程,渲染进程负责解析HTML/CSS、执行JS、处理事件、布局/绘制等。比较特殊的是,渲染进程不同于其它进程,只有一个渲染主线程

浏览器的渲染进程之所以不采用多线程处理任务,主要原因涉及数据安全、执行效率及系统稳定性等多方面因素。

浏览器的心跳:事件循环

既然浏览器只有一个渲染主线程,那么在处理这么多的任务的时候如何进行调度呢? 比如:

  • 浏览器正在执行一个js函数 此时有用户点击按钮触发了事件 我应该立刻去执行回调函数吗
  • 浏览器正在执行一个js函数 此时有一个计时器到达了指定时间,我应该立刻去执行他的回调吗
  • 用户点击了一个按钮 此时有一个计时器到达了指定时间,我应该处理哪一个函数呢

为了解决以上问题 浏览器想出了一个绝妙的办法: 排队

image.png

  1. 渲染主线程会进入一个while循环 不断从消息队列中提取第一个任务执行
  2. 其它线程可向消息队列中添加任务到尾部

这样的整个过程称为事件循环,也叫消息循环。但是代码在执行过程中会遇到一些无法立刻处理的任务,如果让代码停下等待,就会造成渲染主线程堵塞,影响效率和用户体验,因此浏览器会采用异步任务来解决。

异步的魔法

在遇到下面这段代码时 如果不考虑异步,页面会在执行到该函数时 “卡住”

setTimeout(()=>{
  console.log("砸瓦鲁多")
},3000)

为了避免堵塞 提高效率 浏览器提出了异步的解决方案,就像你在准备做饭,你会等水烧开再去切菜,还是在等水烧开的过程中去切菜,答案是显而易见的。我们来简单看一下浏览器怎么实现异步:

  1. 渲染的过程中,为了避免堵塞。浏览器会先执行全局js将任务排序。
  2. 在遇到无法立刻处理的任务时:将调用一个其它线程进行监听,当条件满足时将任务加入到异步队列中。
  3. 当消息队列里为空时,异步队列就会将任务按顺序添加到消息队列中执行。

image.png

以上 通过异步就可以实现渲染主线程永不堵塞

任务的vip通道

那么异步的渲染任务的时候有没有优先级呢? 有的兄弟有的

之前或许你听过微任务和宏任务,这就是之前的异步任务优先级分类。根据w3c的最新标准,已不再使用传统的“宏任务”分类,转而采用更灵活的多队列模型。任务可以分为多个类型,一个队列中可以有多个类型任务,但是同一类型的任务必须在一个队列中。

根据队列的不同,任务调度就像医院的急诊分级:

队列类型类比典型任务优先级标志
微队列危重病抢救室Promise.then()🚨🚨🚨🚨🚨
交互队列急诊处置室click/scroll事件🚨🚨🚨🚨
延时队列普通门诊setTimeout/setInterval🚨🚨🚨

image.png

由此我们可以知道开篇代码的执行顺序:

  • 同步代码立即执行 → 打印3
  • 微队列优先处理 → 打印2
  • 最后处理延时队列 → 打印1

当一个异步任务满足回调时会被放到相应的队列中,在渲染主线程运行完全局js后,会优先运行微队列中的所有任务,再运行交互队列的所有任务,最后再运行延时队列里的任务。

结语

讲到这里 事件循环基本就结束了。

总结一句:渲染单进程是异步产生的原因,而事件循环是异步的实现方式。

如果你有独到见解或踩坑经历,欢迎在评论区分享,感谢包涵