JavaScript异步(四)事件循环机制
本文最后更新于:8 个月前
什么是「JavaScript事件循环机制」?JavaScript中异步任务分为哪两种?事件循环机制对代码执行顺序如何影响?
JavaScript异步(四)事件循环机制
一、「事件循环机制」简介
JavaScript 是单线程的编程语言,但是在遇到异步事件的时候,js线程并没有阻塞,还会继续执行。依靠的就是JS的事件循环机制。同时它也是JS并发模型的基础。是用来协调各种事件、用户交互、脚本执行、UI 渲染、网络请求等的一种机制。
JavaScript在执行的时候有一个「执行上下文栈」,在代码执行过程中,通过将不同函数的执行上下文压入栈中来保障代码有序执行。在执行同步代码过程中,如遇到异步事件,JS引擎会将异步事件交给「WebAPI」去完成。当异步事件完成后,分配一个线程去通知主线程“刚才的异步任务完成了”(实质上是把回调函数放入CallbackQueue「任务队列」中)。当「执行上下文栈」空了时,就把CallbackQueue「任务队列」中的一个任务放入栈中,然后重复执行上述过程。
二、CallbackQueue任务队列
在JavaScript中,异步任务分为两种:宏任务、微任务。执行顺序:微任务->宏任务。
当「执行上下文栈」的事件执行完后,首先检查微任务队列是否有任务,若有就移入执行上下文栈执行。当微任务栈空了后,再检查宏任务队列是否有任务,如果有,就搬入一个任务到栈中执行,然后先检查此时的微任务栈并进行处理,然后再继续执行宏任务栈中剩余的任务。直到两个队列都为空时结束。
2.1、宏任务
宏任务包括:执行全局代码、解析HTML、创建主文档对象、更改当前URL、定时器事件、以及各种事件(页面加载、输入、网络加载)。从浏览器的角度来说,宏任务代表一个个独立的工作单元。
常见的被加入「宏任务队列」的操作:
- 定时器事件:setTimeout()、setInterval()
- DOM事件
- UI渲染
- AJAX,网络异步请求
2.2、微任务
微任务需要尽可能快地、通过异步方式执行,同时不能产生全新的微任务。微任务可以使得在重新渲染UI前执行指定的行为,避免不必要的重绘。
常见的被加入「微任务队列」的操作:
- queueMircotask
- MutationObserver(变异观察者)
- Promise.resolve()
三、浏览器中的JS线程
3.1、进程 vs 线程
- 进程:计算机中正在运行的程序。一个进程中可以有多个线程
- 线程:操作系统运算调度的最小单位。每一个进程中都会至少创建一个线程用来执行程序中的代码(叫主线程)
计算机如果只有一个CPU核心,但是却要多个进程同时工作,该如何实现呢?
答案是将CPU计算时间进行「切片」,让不同进程的线程轮流获得「时间片」,这样就可以让用户感觉起来像是多个进程在同步工作。
3.2 、JS线程
JavaScript是单线程的,它的容器是「浏览器进程」或「Node进程」
浏览器进程中有一个单独运行JavaScript代码的线程,我们在此称其为「JS线程」。但是JS线程同一时间只能完成一个任务,如果遇上了非常耗时的操作,那么就会阻塞线程,使其它JS代码无法响应。
事实上,耗时的操作都不是由JS线程执行的。浏览器进程除了JS线程外,还有其它线程,如「渲染线程」、「定时触发器进程」等等
3.3、扩展:浏览器进程线程介绍
浏览器进程包括:
- Browser进程,负责协调主控,只有一个。主要作用:(1)管理其它进程;(2)负责浏览器用户交互、如前进后退;(3)文件下载等等
- GPU进程,只有一个,可禁用掉,用于3D绘制
- 第三方插件进程
- Renderer进程,通常所说的浏览器内核,一个标签页对应一个Renderer进程,包含我们经常接触到的「JS线程」、「GUI渲染线程」等。
Rederer进程(常说的浏览器内核)的线程包括:
- GUI渲染线程,作用:解析HTML,构建DOM树、CSS树、Renderer树,计算布局和绘制
- JS引擎线程,作用:执行JavaScript脚本,与GUI渲染线程互斥
- 事件触发线程,作用:控制DOM事件循环,「事件队列」就在这个线程中
- 定时器触发线程,setTimeout和setInterval所在的线程
- 异步http请求线程
参考:[前端进阶] - 搞懂浏览器进程和线程 - 掘金 (juejin.cn)
四、事件循环实例分析
以下部分实例需要对Promise知识较为熟悉,建议先熟悉Promise基础之后再进行分析。
3.1、简单的宏任务和微任务
注意:new Promise(…)中的代码为同步代码,then()后面的代码才是异步代码(微任务)
1 |
|
3.2、resolve()将微任务加入队列
**resolve()才是把微任务加入队列的操作,而不是then()**,then()方法只是用来声明onResolved、onRejected处理程序
1 |
|
3.3、包含async和await的情况
async 和 await 其实就是 Generator 和 Promise 的语法糖。async 函数和普通 函数没有什么不同,他只是表示这个函数里有异步操作的方法,并返回一个 Promise 对象
1 |
|
1 |
|
1、当前微任务:[],宏任务:[<script>
]
- 输出async1 start
执行Promise.resolve(async2())
,同时resolve()把then后面的处理程序加入微任务队列- 遇到timeout,加入宏队列
new Promise(executor)
executor函数里面的代码属于同步代码,立即执行,所以输出promise1,执行resolve(),将then(promise2)加入微队列- 输出script end
2、当前微任务:[‘async1 end’, ‘promise2’],宏任务:[‘timeout’]
- 输出async1 end
- 输出promise2
3、当前微任务:[],宏任务:[timeout]
- 输出timeout
3.4、Promise.reject的情况
如果没有在then中指定onReject处理程序,那么rejected的Promise实例会被Promise.resolve向后继承,直到在then参数中遇到onResolved处理程序,或者遇到catch
1 |
|
3.5、复杂综合情况
如果then参数中处理函数返回的不是普通值,而是thenable或者Promise,就会被延迟到下一次微任务执行
1 |
|
第一轮,Promise.resolve()各自把then后的处理程序加入微任务队列。执行完第一轮后的微任务队列前后有两个元素
- A,{console.log(0); return Promise.resolve(4)}
- B,{console.log(1)}
第二轮,执行代码A,输出0,接着return遇到Promise,被延迟到下一次微任务执行
然后执行代码B,输出1,隐式调用了Promise.resolve()。此时微任务队列有两个个元素:
C,{return Promise.resolve(4)}
D,{console.log(2)}
第三轮,执行代码C,把C的then处理程序(实际上为空)加入微任务队列
然后执行代码D,输出2,把D的then处理程序加入微任务队列
- E,{}
- F,{console.log(3)}
第四轮,执行代码E,此处内部是如何操作的,我暂时不明白,但是结果是把A的then处理程序加入了微任务队列
然后,执行F,把F的then处理程序加入微任务队列
- G,{console.log(4)}
- H,{console.log(5)}
第五轮,执行G,输出4,后面没有then处理程序了
然后执行H,输出5,把H的then处理程序加入微任务队列
- I,{console.log(6)}
第六轮,执行I,输出6,后面没有then处理程序了,结束。
与上例做个对比,将 return Promise.resolve(4) 改为 return 4,看看结果有何不同
1 |
|
还是这个例子,再看一种不同的情况
1 |
|