前言
不知道大家在看到下面这段代码的时候会想到输出是什么
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运行中用于处理异步任务的编程模型。
首先我们先来简单了解一下什么是进程和线程。
进程是指在一块独立的内存中运行的程序。
线程是指程序中负责完成任务的单元,一个进程通常有多个线程。
我们可以在浏览器中点击更多任务,找到浏览器任务管理,在这里面可以看到浏览器的所有进程。
在浏览器中存在多个进程,其中每一个页面都是一个渲染进程,渲染进程负责解析HTML/CSS、执行JS、处理事件、布局/绘制等。比较特殊的是,渲染进程不同于其它进程,只有一个渲染主线程。
浏览器的渲染进程之所以不采用多线程处理任务,主要原因涉及数据安全、执行效率及系统稳定性等多方面因素。
浏览器的心跳:事件循环
既然浏览器只有一个渲染主线程,那么在处理这么多的任务的时候如何进行调度呢? 比如:
- 浏览器正在执行一个js函数 此时有用户点击按钮触发了事件 我应该立刻去执行回调函数吗
- 浏览器正在执行一个js函数 此时有一个计时器到达了指定时间,我应该立刻去执行他的回调吗
- 用户点击了一个按钮 此时有一个计时器到达了指定时间,我应该处理哪一个函数呢
为了解决以上问题 浏览器想出了一个绝妙的办法: 排队
- 渲染主线程会进入一个while循环 不断从消息队列中提取第一个任务执行
- 其它线程可向消息队列中添加任务到尾部
这样的整个过程称为事件循环,也叫消息循环。但是代码在执行过程中会遇到一些无法立刻处理的任务,如果让代码停下等待,就会造成渲染主线程堵塞,影响效率和用户体验,因此浏览器会采用异步任务来解决。
异步的魔法
在遇到下面这段代码时 如果不考虑异步,页面会在执行到该函数时 “卡住”
setTimeout(()=>{
console.log("砸瓦鲁多")
},3000)
为了避免堵塞 提高效率 浏览器提出了异步的解决方案,就像你在准备做饭,你会等水烧开再去切菜,还是在等水烧开的过程中去切菜,答案是显而易见的。我们来简单看一下浏览器怎么实现异步:
- 渲染的过程中,为了避免堵塞。浏览器会先执行全局js将任务排序。
- 在遇到无法立刻处理的任务时:将调用一个其它线程进行监听,当条件满足时将任务加入到异步队列中。
- 当消息队列里为空时,异步队列就会将任务按顺序添加到消息队列中执行。
以上 通过异步就可以实现渲染主线程永不堵塞
任务的vip通道
那么异步的渲染任务的时候有没有优先级呢? 有的兄弟有的
之前或许你听过微任务和宏任务,这就是之前的异步任务优先级分类。根据w3c的最新标准,已不再使用传统的“宏任务”分类,转而采用更灵活的多队列模型。任务可以分为多个类型,一个队列中可以有多个类型任务,但是同一类型的任务必须在一个队列中。
根据队列的不同,任务调度就像医院的急诊分级:
队列类型 | 类比 | 典型任务 | 优先级标志 |
---|---|---|---|
微队列 | 危重病抢救室 | Promise.then() | 🚨🚨🚨🚨🚨 |
交互队列 | 急诊处置室 | click/scroll事件 | 🚨🚨🚨🚨 |
延时队列 | 普通门诊 | setTimeout/setInterval | 🚨🚨🚨 |
由此我们可以知道开篇代码的执行顺序:
- 同步代码立即执行 → 打印3
- 微队列优先处理 → 打印2
- 最后处理延时队列 → 打印1
当一个异步任务满足回调时会被放到相应的队列中,在渲染主线程运行完全局js后,会优先运行微队列中的所有任务,再运行交互队列的所有任务,最后再运行延时队列里的任务。
结语
讲到这里 事件循环基本就结束了。
总结一句:渲染单进程是异步产生的原因,而事件循环是异步的实现方式。
如果你有独到见解或踩坑经历,欢迎在评论区分享,感谢包涵