事件循环,如此简单
事件循环(Event Loop)
事件循环是---指浏览器或者Node解决JavaScript单线程运行时不会堵塞的一种机制
-
浏览器或者Node
也就是说根据JavaScript的运行环境,会有一些不同的地方,这个放到后面介绍
-
JavaScript单线程(一个主线程)
为什么不能设计成多线程语言呢?JavaScript设计出来时是为了处理用户交互、网络和操作DOM等需求的,如果是多线程会带来很复杂的同步问题,假设有两个主线程,一个线程在某个DOM节点添加内容,另一个线程删除这个节点,这个时候JavaScript应该怎么办?
-
不会堵塞
那既然是单线程,那前面的事件如果超级耗时,那不就堵塞了么?要一直等待么?所以JavaScript事件循环中异步的概念就是为了解决阻塞问题,也是事件循环的核心知识
调用栈(Call Stack)
所有的任务都会被放到调用栈,在调用栈中按照顺序等待主程序依次执行
-
所有任务
包括同步任务和异步任务
- 同步任务---在主程序上排队执行的任务(一般来说调用之后很快就能得到结果,我们采取同步策略)
- 异步任务---等主程序上同步任务全部执行完毕才执行的任务(一般来说调用之后需要过一段时间才能的到结果,我们采取异步策略)
-
什么时候放到调用栈的
JS引擎按照顺序解析代码,遇到函数调用,入栈 (遇到函数声明,入堆)
-
调用栈
后进先出的有序集合,这是代码执行的地方
-
主程序
JS引擎,负责处理 JavaScript脚本,执行代码
-
执行
- 同步函数执行顺序----入栈,直接执行,执行完得到结果后弹出栈,继续下一个函数调用
function one(){ console.log("执行1"); console.log("执行2"); } one()
-
异步函数执行顺序----入栈,分给Web APIs,弹出栈,继续下一个函数调用
Web API:是浏览器提供的一套操作浏览器功能和页面元素的API(DOM和BOM,其中包含处理JS异步的方法)。Web API一般都有输入和输出(函数的传参和返回值),Web API很多都是方法(函数)
处理异步任务时,入栈之后提交给对应的异步API处理,然后出栈
注:异步API(web API中处理异步的API,比如说 事件监听函数、DOM、HTTP/AJAX请求、setTimeout等等)
function two() { setTimeout(() => { console.log("执行1"); }, 100); setTimeout(() => { console.log("执行2"); }, 100) } two()
- 流程图
异步处理流程
交给异步API处理的异步任务,会有一个什么样的流程呢?
简单来说,异步任务不是连续完成的,先执行第一段,等第一段执行完放入队列做好准备,等主程序执行第二段,第二段也被叫做回调。
任务队列
按照先进先出的顺序存储着所有异步任务的回调函数,任务队列分为两种:宏任务队列(Task Queue) 和 微任务队列(Microtask Queue) ,对应的里面存放的是宏任务和微任务;
为什么要分宏任务和微任务?
-
宏任务是进程之间的切换,速度慢,且每次执行需要切换上下文。因此一个Eventloop中只执行一个宏任务。由宿主发起,根据环境不同,宏任务由浏览器或node发起的。
-
微任务是线程之间的切换,速度快。不用进行上下文切换,可以快速的一次性做完所有的微任务。微任务是JavaScript自身发起的。
在ES3以及以前的版本中,JavaScript本身没有发起异步请求的能力,需要浏览器来做,也就没有微任务的存在。在ES5之后,JavaScript引入了Promise,这样,不需要浏览器,JavaScript引擎自身也能够发起异步任务了,也就有了微任务。据此也可以知道宏任务和浏览器有关,微任务和JavaScript自身有关
什么是进程和线程?
我们前面说的JS是单线程,指的是一个进程里只有一个主线程;
官方的说法是:进程是CPU资源分配的最小单位;线程是CPU调度的最小单位;
- 每个进程都有单独属于自己的CPU资源
- 一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线
- 每个进程都是独立存在的
- 每个线程都可以用自己所属进程的资源,因为一个进程内的内存空间是共享的
怎么分辨宏任务和微任务?
宏任务是由宿主发起的,而微任务是由 JS 本身发起的
宏任务 | 浏览器 | Node |
---|---|---|
整体代码(script) | ✅ | ❌ |
setTimeout | ✅ | ✅ |
setInterval | ✅ | ✅ |
setImmediate | ❌ | ✅ |
异步Ajax请求 | ✅ | ✅ |
I/O | ✅ | ✅ |
UI交互事件 | ✅ | ❌ |
requestAnimationFrame | ✅ | ❌ |
postMessage | ✅ | ❌ |
MessageChannel | ✅ | ❌ |
微任务 | 浏览器 | Node |
---|---|---|
Promise.then 、catch、finally | ✅ | ✅ |
MutationObserver(对 Dom 变化监听) | ✅ | ❌ |
process.nextTick | ❌ | ✅ |
事件循环中宏任务和微任务怎么执行?
当调用栈为空时,主程序就检查任务队列中是否有任务,如果有任务就按队列顺序入栈运行,这个过程不断重复,就是事件循环;
由于任务队列有宏任务队列和微任务队列两种,宏任务和微任务会有什么不一样的操作呢?
- 每次循环都会把微任务队列里的任务执行完的
- 每次循环都只会执行宏任务队列第一个任务
- 每执行完一个宏任务执行,调用栈就是空的,就会检查微任务队列是否有任务,如果有则把微任务执行完,再执行下一个宏任务,如果没有则直接执行下一个宏任务
- 由于前面说到
script
整体代码也是一个浏览器宏任务,当我们第一次执行时,主程序就从宏任务队列取script
宏任务开始执行,然后执行宏任务下的所有同步任务,script
宏任务结束执行,调用栈为空,执行所有微任务,执行下一个宏任务......
- 如果在执行微任务的过程中,产生新的微任务添加到微任务列表,也需要一起清空;微任务没清空之前,不会执行下一个微任务
- 初始的时候,调用栈是空的,微任务队列也是空的,只有宏任务队列有任务,所以......
看懂这块代码基本就懂了
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<script>
function first() {
console.log("first开始");
setTimeout(() => {
console.log("开始第二轮宏任务");
new Promise((resolve, reject) => {
resolve("执行第二轮宏任务后的微任务1")
}).then((res => {
console.log(res);
new Promise((resolve, reject) => {
resolve("这是执行第二轮宏任务种微任务1时,所产生的新的微任务,也会在本次执行完再执行下一轮宏任务")
}).then((respone => {
console.log(respone);
}))
}))
function reoloveTest() {
console.log('reolove是同步的哦,不要搞错了哦');
return "执行第二轮宏任务后的微任务2"
}
new Promise((resolve, reject) => {
resolve(reoloveTest())
}).then((res => {
console.log(res);
}))
setTimeout(() => {
console.log("开始第四轮宏任务");
console.log("结束第四轮宏任务,没有微任务了,也没有宏任务了,结束");
})
console.log("结束第二轮宏任务,有微任务,准备执行微任务");
}, 0);
setTimeout(() => {
console.log("开始第三宏任务");
Promise.resolve(console.log("再次提醒resolve里的代码是同步任务哦"))
new Promise((resolve, reject) => {
resolve("执行第三轮宏任务后的微任务1")
}).then((res => {
console.log(res);
}))
new Promise((resolve, reject) => {
resolve("执行第三轮宏任务后的微任务2,有宏任务,准备执行宏任务")
}).then((res => {
console.log(res);
}))
console.log("结束第三轮宏任务,有微任务,准备执行微任务");
}, 0);
new Promise((resolve, reject) => {
resolve("执行微任务1")
}).then((res => {
console.log(res);
}))
console.log("first结束");
}
function second() {
console.log("second开始");
new Promise((resolve, reject) => {
resolve("执行微任务2,微任务执行完,取宏任务执行")
}).then((res => {
console.log(res);
}))
console.log("second结束");
}
console.log("开始第一轮script宏任务");
first()
second()
console.log("结束第一轮script宏任务,调用栈空,检查有微任务,取微任务执行");
</script>
<body>
</body>
</html>
上面代码有基础需要注意
-
Promise.resolve(console.log("再次提醒resolve里的代码是同步任务哦"))
是同步的,相当于awit console.log("再次提醒resolve里的代码是同步任务哦")
,即awit后面接的任务是同步任务async function asyncFn() { console.log('async start'); await logInfo() console.log('async end'); } function logInfo() { console.log('年轻人,千万不要赌球'); } asyncFn() console.log("记住啊,千万别赌球"); 相当于 function asyncFn() { console.log('async start'); new Promise((resolve, reject) => { resolve(logInfo()) }).then(() => { console.log('async end'); }) } function logInfo() { console.log('年轻人,千万不要赌球'); } asyncFn() console.log("记住啊,千万别赌球");
-
执行微任务的过程中,产生新的微任务添加到微任务列表,也需要一起清空;微任务没清空之前,不会执行下一个微任务
一些疑问
为何和定时器有关的任务是宏任务?
因为计时是实时的,它必定不能被阻塞,时器被设计在另外一个进程中被管理,因此,定时器任务会有进程的切换,所以是宏任务
把定时器想成一个时间管理中心,而后在上面注册一个个任务,这些任务自己和时间无关,时间管理中心和时间有关的,当时间管理中心发现时间到了,要执行任务,就从任务列表中找出注册的任务,并通知JS执行任务,因此能够看到,时间管理中心(定时器的进程)和执行的任务(JS运行时)是无关的,不共享上下文,因此是宏任务
事件为何是宏任务呢?
事件的触发是依赖于浏览器的实现,平台有它本身的事件注册和派发机制,事件的独立注册表和派发机制致使,他也不会和JS存在一个进程中,事件的管理中心必定是在另一个进程中实现的,也就是宏任务
那么像Observer(如MutationObserver等),和一些渲染为何都是微任务呢?
虽然它们和JS的自己无关,可是它们的执行时机和它们所在的进程是有关的
好比MutationObserver,观察的是DOM,它的做用便是对DOM的变化作出响应,因此,他会在管理DOM的进程中
渲染也是同样的,是在整个渲染流程中的某一步做的回调,并无切换出它的自己所在的空间
微任务在执行时,它能获取到任务外的上下文,宏任务在执行时,他不能获取到任务外的上下文
浏览器和Node的事件循环差异
下次一定
转载自:https://juejin.cn/post/7170176140395413540