JS的事件循环机制
一、JS的运行机制
1.1 简介
JavaScript是一门单线程
语言,而单线程就意味着在一个时间点内只能做一件事情,这样就造成了这门语言的一些局限性。举个例子:如果按照单线程同步的方式运行时,当一个页面加载时向后端请求接口数据,等待数据从服务器返回的过程中,页面会呈现空白屏幕的效果。因为JavaScript是单线程,这就导致页面加载阻塞,后面的结构代码无法运行。但是在实际的网站打开时很少会发现页面假死的现象,这些都要归功于JavaScript的事件处理机制
。
1.2 同步与异步的产生原因
上面简介说过了JS是单线程的,同一时间只能做一件事,这样的话JS引擎就会有很多空余的时间;那么作为JS语言的开发者为了解决单线程所造成的的这个问题,提出了同步和异步的执行模式;
1.3 同步(阻塞)
同步执行模式就是JS会严格按照原本的单线程逻辑,从上到下、从左到右的执行代码逻辑。上一个未执行完,下一个就不会执行。请看下面这个例子:
let num = 0;
while(true) {
num++;
console.log(num)
}
// 上面是一个while死循环,永远都不会执行下面的这段代码
alert("代码执行完毕");
1.4 异步(非阻塞)
异步执行模式就是为了解决JS单线程的所带来的问题,从字面意思理解异步就是与同步相反,所以异步任务不会按照默认顺序执行。在执行JS代码时先逐行执行同步代码,遇到异步代码先将它"挂起",直到同步代码执行完毕或者特定时间后就执行异步代码。
let num = 0;
setTimeout(()=>num++,500);
console.log(num); // 0
// 上面的setTimeout属于异步任务,所以当代码执行到第二行时会将它挂起
// 然后执行第三行代码直接输出0,而当500ms过去之后num将变为1
1.5 同步与异步总结
JS执行顺序就按照单线程模式运行,同步先执行,异步后执行。所有的异步任务都要等待当前的同步任务完成之后执行。
同步通俗讲就是:排队做核酸,前一个做完后一个才能做,最后来的人排到最后,遵从先进先出的规则;
异步通俗讲就是:去饭店点菜,由服务员记录每桌点的菜品,之后交给后厨;后厨根据菜单同时炒三桌相同的菜,最后三桌人都能同时吃到这个菜,不需要一桌一桌的做,减少等待时间。
二、JS的运行原理
2.1 JS的线程组成
通过上面的了解我们知道JS是单线程的,但是在浏览器中一个tab页面实际上是以多个线程协助操作JS单线程异步执行的,因此我们要了解具体的线程:
- GUI渲染线程 // 识别css、js、html文件生成文档树最终绘制到屏幕上
- JavaScript引擎线程 // 执行js代码
- 定时器触发线程 // 监听定时器,当时间到时触发
- http请求线程 // 接口请求
- 事件触发线程 // 用户与界面交互时触发的事件监听
- 其他线程
通过真实的浏览器线程组成我们可以知道实际运行的JS线程并不是一个,那为什么称它为单线程语言呢?因为在JS代码运行时只存在一个活动线程,然后要实现同步异步就需要多线程切换的方式来实现,所以JS线程又可以分为主线程:执行JS代码
和工作线程:处理异步任务的执行
。
2.2 JS运行模型
上面根据左侧代码画出了一个简单的运行模型:首先需要说明的是setTimeout属于异步任务。左侧代码声明了四个函数然后依次执行;进入JS主线程
时遇到同步代码就直接放入执行栈
中执行,执行完毕后如果没有再次引用则出栈就销毁。异步代码先放入工作线程
中等待;当异步代码有结果返回时就会放到任务队列
中排队,同步任务执行完成之后,执行栈
会在任务队列
中执行异步任务。这里我们看到3比2先输出,这是因为test2中定时器是1000ms而test3是500ms,所以test3先进入任务队列
,先进先出;
2.3 总结
根据上面的运行模型图例相信我们对于代码的同步异步执行有了一个初步的认识,其中异步任务最终都会进入到任务队列
中排队等待执行,而任务队列
中又将异步任务划分为宏任务
与微任务
。
三、JS中的宏任务与微任务
3.1 宏任务与微任务介绍
通过上面我们了解到异步任务首先都会进入到JS工作线程
中进行等待,当有异步结果返回时进入任务队列
。任务队列
中的异步任务⼜分为【宏任务】和【微任务】。
3.2 宏任务
宏任务是JS最先产生异步任务,包括setTimeout、setInterVal、AJAX等,在代码主线程
中按照同步代码的顺序,逐个进⼊⼯作线程
挂起,再按照异步任务到达的时间节点,逐个进⼊异步任务队列
,最终按照队列中的 顺序进⼊函数执⾏栈
进⾏执⾏。
3.3 微任务
微任务是新提出的异步任务,微任务在异步任务队列
的基础上增加了微任务的概念,每⼀个宏任务执⾏前,程序会先检测中是否有当次事件循环未执⾏的微任务,优先清空本次的微任务后,再执⾏下⼀个宏任务,每⼀个宏任务内部可注册当次任务的微任务队列,再下⼀个宏任务执⾏前运⾏,微任务也是按照进⼊队列的顺序执⾏的。
3.4 常见的宏任务与微任务
宏任务 | 微任务 |
---|---|
整个script标签 | MutationObserver(浏览器) |
I/O操作 | process.nextTick(node) |
setTimeout | Promise |
setInterval | Promise.then Promise.catch finally |
setImmediate(node环境下) | async await |
requestAnimationFrame(浏览器) |
3.5 总结
JS代码执行流程:
- 默认的同步代码按照顺序从上到下,从左到右运⾏,运⾏过程中将异步任务存放到工作进程中
- 注册本次的微任务和后续的宏任务
- 执⾏本次同步代码中注册的微任务,并向任务队列注册微任务中包含的宏任务和微任务
- 将下⼀个宏任务开始前的所有微任务执⾏完毕
- 执⾏最先进⼊队列的宏任务,并注册当次的微任务和后续的宏任务,宏任务会按照当前任务队列的队尾继续向 下排列