一、JS特点
Js作为一门单线程语言,即一次只能完成一个任务,当有多个任务时,任务就得进行排队等待执行,只能等待自己的前一个任务执行完成后自己才能执行。
二、JS事件循环
要理解JS的事件循环的就必须理解JS的执行过程、JS如何实现多线程、JS的同步、异步任务(宏任务、微任务)
1、JS的执行过程
<script>
//js主线程开始
console.log(1);
console.log(2);
//js主线程结束
</script>
ps:JS引擎线程从上往下解析代码,依次执行代码,在控制台打印出1和2
那么问题来了?看下面的代码
<script>
//js主线程开始
console.log("加载了顶部导航栏");
setTimeout(() => {
console.log('加载了轮播图图片');
}, 60000)
console.log("加载了网页的主体业务区");
//js主线程结束
</script>
ps:如果JS没有借助浏览器实现多线程,那么上面的代码执行就是一个网页先加载导航栏,再花费
1分钟加载轮播图(图片加载花费时间长),然后再加载出网页的主体业务区域,试想一下,用户主要操作的就是主体业务区域,而一打开网页需要等待1分钟后网页主体业务区域页面才加载出来,用户体验差,用户早都关闭页面了。
那么JS肯定不会让上面的情况出现的,实际上上面代码实际运行是先加载顶部导航栏,再加载网页主体业务区域,过1分钟后轮播图再加载出来。
下面就重点讲解JS是如何解决上面的问题?
像比较常见的,定时器,延时器,ajax发送网络请求,node读取文件,都需要花费较多的时间,执行JS代码的JS引擎线程总不能因为某个任务花费大量时间而继续等待吧?(会造成页面渲染慢,交互卡顿等问题,用户体验极差)
2、JS如何实现多线程?
JS语言本身就是单线程,但是JS的宿主环境,浏览器和Node能够实现多线程。
JS利用浏览器可以实现多线程,先来看看浏览器中的一些线程
浏览器内核(负责渲染页面的进程)
① 图形用户界面GUI渲染线程
负责渲染浏览器界面,包括解析HTML、CSS、构建DOM树、Render树、布局与绘制等
当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行
② JS引擎线程
JS内核,也称JS引擎,负责处理执行javascript脚本代码,浏览器无论什么时候都只有一个JS引擎在运行JS程序
③ 事件触发线程
听起来像JS的执行,但是其实归属于浏览器,而不是JS引擎,用来控制时间循环(可以理解,JS引擎自己都忙不过来,需要浏览器另开线程协助)
当JS引擎执行代码块如绑点事件(如鼠标点击)、AJAX异步请求等,对应的回调函数会先进入异步任务注册表中进行函数注册,当对应的事件符合触发条件(被点击,ajax就是请求成功或者失败后)被触发时,该线程就会通知异步任务注册表,然后异步任务注册表把相应回调函数添加到待处理任务队列的队尾,等待JS引擎执行完同步任务后再进行处理。
注意:由于JS的单线程关系,所以这些待处理任务队列中的回调函数都得排队等待JS引擎处理(当JS引擎空闲时才会去执行)
④ 定时触发器线程
setInterval与setTimeout所在线程
定时计时器并不是由JS引擎计时的,而是由定时触发器线程进行计时;当JS引擎线程执行setInterval代码块时,会调用定时触发器线程进行计时,同时将定时器的回调函数在异步任务队列函数注册表中进行注册,当时间计时完毕后,定时触发器线程就会通知异步任务队列函数注册表,然后异步任务队列函数注册表就将相应的回调函数送入异步任务队列队尾中,等待JS引擎空闲了执行
⑤ 异步HTTP请求线程
Js引擎线程在执行到ajax代码块的时候,利用XMLHttpRequest在连接后启动一个新的异步HTTP请求线程,同时向异步任务队列函数注册表进行回调函数的注册,线程如果检测到请求的状态变更,该线程同样会通知异步任务队列函数注册表,同理,异步任务队列函数注册表就将相应的回调函数送入异步任务队列队尾中,等待JS引擎空闲了执行。
执行流程图:
下面结合具体案例:
案例1:
还是上面的案例:
<script>
//js主线程开始
console.log("加载了顶部导航栏");
setTimeout(() => {
console.log('加载了轮播图图片');
}, 60000)
console.log("加载了网页的主体业务区");
//js主线程结束
</script>
ps:代码执行过程:
JS引擎线程从上往下解析执行:
①console.log("加载了顶部导航栏")为同步任务,JS引擎线程直接执行
②遇到setimeout代码块,为异步任务,JS引擎线程通过浏览器请求定时器线程进行帮助,定时器线程开始帮忙计时,同时将延时器的回调函数在异步任务队列函数注册表(Event table)中进行注册,计时完成后定时器线程通知Event table,Event table将回调函数压入异步任务队列(Event queue)中等待JS引擎线程执行。
③console.log("加载了网页的主体业务区")为同步任务,JS引擎线程直接执行
④JS引擎线程空闲,读取异步任务队列(Event queue)中的函数在JS引擎线程中执行(console.log('加载了轮播图图片');),如果此时定时器的函数还没有进入异步任务队列(Event queue),那么js引擎线程会持续去查看异步任务队列是否有任务,有就压入JS引擎线程进行执行
案例2:
<script>
//js主线程开始
console.log("同步任务1");
$.ajax({
url: '',
data,
success: (res) => {
console.log('后端返回数据'+res);
}
})
console.log("同步任务2");
//js主线程结束
</script>
ps:代码执行过程:
JS引擎线程从上往下解析执行:
①console.log("同步任务1")为同步任务,JS引擎线程直接执行
②遇到ajax代码块,为异步任务,JS引擎线程通过浏览器请求异步http请求线程进行帮助,异步http请求线程开始帮忙发起http请求,同时将ajax的success回调函数在异步任务队列函数注册表(Event table)中进行注册,请求完成后定时器线程通知Event table,Event table将回调函数压入异步任务队列(Event queue)中等待JS引擎线程执行。
③console.log("同步任务2")为同步任务,JS引擎线程直接执行
④JS引擎线程空闲,读取异步任务队列(Event queue)中的函数( console.log('后端返回数据'+res))在JS引擎线程中继续执行,js引擎线程会持续去查看异步任务队列是否有任务,有就压入JS引擎线程进行执行
3、JS同步任务、异步任务(宏任务,微任务)
事件循环:
①由所有宏任务和在执行宏任务期间产生的所有微任务组成。完成当下的宏任务后,会立刻执行所有在此期间产生的微任务。(重点理解)
②JS引擎线程当执行到异步任务的时候,在等待异步任务的同时,JS引擎去执行其他同步任务,等到异步任务准备好了,再去执行回调。这种模式的优势显而易见,完成相同的任务,花费的时间大大减少,这种方式也被叫做非阻塞式。
在异步任务中,为什么有了宏任务还需要微任务?
这种设计是为了给紧急异步任务(微任务)一个插队的机会,否则新入队的任务(宏任务)永远被放在队尾。区分了微任务和宏任务后,在事件循环中的一轮宏任务中,微任务实际上就是在插队,这样微任务中所做的状态修改,在下一轮事件循环中也能得到同步。
常见的宏任务有:
script(整体代码)/setTimout/setInterval/setImmediate(node 独有)/requestAnimationFrame(浏览器独有)/IO/UI render(浏览器独有)
常见的微任务有:
process.nextTick(node 独有)/Promise.then()、
Promise.catch()、Promise.final()/Object.observe/MutationObserver
事件循环执行流程:
PS:
首先,执行第一个宏任务:全局Script脚本。产生的的宏任务和微任务进入各自的任务队列中。执行完Script后,把当前的微任务队列清空。完成一次事件循环。
接着再取出一个宏任务进行执行,同样把在此期间产生的的宏任务和微任务进入任务队列中。再把当前宏任务的微任务队列清空。以此往复。
宏任务队列只有一个,而每一个宏任务都有一个自己的微任务队列,每轮循环都是由一个宏任务+多个微任务组成。
前置知识:
- 异步队列分为宏任务队列和微任务队列,对应执行宏任务和微任务
- 宏任务和微任务会被压入宏任务队列和微任务队列的队尾,执行完成后被压出队列,遵循先进先出的规则
-
在执行一个 Promise 对象的时候,当走完
resolve();
之后,就会立刻把.then()
里面的代码加入到微任务队列当中,同理当rejiect()之后,就会立即把.catch()里面的代码加入到微任务队列中 -
任务的一般执行顺序:同步任务 --> 微任务 --> 宏任务。
案例1:
<script>
setTimeout(() => {
//宏任务
console.log('setTimeout宏任务');
}, 0)
new Promise((resolve, reject) => {
console.log("同步任务1");
resolve()//;立即在当前宏任务中开启微任务
console.log("同步任务2");
}).then(res => {
console.log('Promise then 微任务');
})
console.log("同步任务3");
</script>
执行:
执行流程:
①首先JS引擎线程解析执行script标签这个宏任务
②遇到setTimeout代码块,setTimeout为宏任务,将setTimeout压入宏任务队列中
③实例化Promise时遇到console.log("同步任务1")同步任务,直接进行执行打印
④实例化Promise时遇到resolve(),.then()函数为微任务,被压入微任务队列中
⑤实例化Promise时遇到console.log("同步任务2")同步任务,直接进行执行打印
⑥遇到console.log("同步任务3")同步任务,直接进行执行打印
⑦第一轮宏任务script标签执行结束,该宏任务执行期间产生了一个setTimeout宏任务和一个.then()微任务,微任务的优先级比宏任务优先级高,先执行微任务队列中微任务.then(),console.log('Promise then 微任务')直接进行打印,第一轮的宏任务产生的微任务执行结束,第一轮事件循环结束。
⑧因为宏任务队列中还存在setTimeout宏任务,开启第二轮事件循环,执行setTimeout宏任务,
console.log('setTimeout宏任务')直接执行打印,该宏任务没有产生新的宏任务和微任务,该宏任务执行结束发现微任务队列和宏任务队列为空,第二轮事件循环结束
案例2:宏任务和微任务嵌套
<script>
new Promise((resolve, reject) => {
console.log("同步任务1");
setTimeout(() => {
//宏任务
resolve()
console.log("setTimeout宏任务");
}, 0)
console.log("同步任务2");
})
.then(res => {
//微任务
console.log('Promise then 微任务');
})
console.log("同步任务3");
</script>
执行:
执行流程:
①首先JS引擎线程解析执行script标签这个宏任务
②实例化Promise对象,遇到console.log("同步任务1")同步任务,直接打印执行
③遇到setTimeout宏任务,将setTimeout宏任务压入宏任务队列中
④遇到console.log("同步任务2")同步任务执行打印执行
⑤遇到console.log("同步任务3")同步任务执行打印执行
⑥第一轮宏任务script标签执行结束,该宏任务执行期间产生了一个setTimeout宏任务,因为没有产生微任务需要执行,所以第一轮事件循环结束
⑦因为宏任务队列中存在setTimeout宏任务,所以开启第二轮事件循环执行setTimeout宏任务
⑧遇到resolve(),.then()函数为微任务,被压入微任务队列中
⑨执行console.log("setTimeout宏任务")同步任务进行打印
⑩第二轮宏任务setTimeout执行结束,该宏任务执行时产生了一个微任务.then(),对该微任务进行执行,console.log('Promise then 微任务')进行打印,该宏任务产生的微任务也执行完毕,第二轮事件循环结束
案例3:彻底理清宏任务和微任务
<script>
Promise.resolve().then(() => {
console.log("微任务1");
setTimeout(() => {
console.log("宏任务2");
}, 0)
})
setTimeout(() => {
console.log("宏任务1");
Promise.resolve().then(() => {
console.log("微任务2");
})
}, 0)
</script>
执行结果:
执行流程:
①首先JS引擎线程解析执行script标签这个宏任务
②遇到
Promise.resolve().then(() => {
console.log("微任务1");
setTimeout(() => {
console.log("宏任务2");
}, 0)
})
这个微任务,压入微任务队列中
③遇到
setTimeout(() => {
console.log("宏任务1");
Promise.resolve().then(() => {
console.log("微任务2");
})
}, 0)
第一个宏任务,压入宏任务队列
④第一轮宏任务script标签执行结束,该宏任务执行期间产生了一个微任务和一个宏任务,马上执行该宏任务产生的微任务
console.log("微任务1");
setTimeout(() => {
console.log("宏任务2");
}, 0)
打印出微任务1,同时又产生了一个宏任务,把宏任务压入宏任务队列的队尾,此时宏任务队列存在2个宏任务,第一轮事件循环结束
⑤此时宏任务队列中存在两个宏任务,任务队列遵循先进先出的原则,先执行第一个宏任务
console.log("宏任务1");
Promise.resolve().then(() => {
console.log("微任务2");
})
打印出宏任务1,产生了微任务,第一个宏任务执行完毕后马上执行该宏任务产生的微任务
console.log("微任务2");
打印出微任务2,第二轮事件循环结束
⑥此时宏任务队列还存在一个宏任务
setTimeout(() => {
console.log("宏任务2");
}, 0)
执行该宏任务,打印出宏任务2,该宏任务没有产生微任务,第三轮事件循环结束
三、 总结
1、js是单线程语言,即一次只能完成一个任务,当有多个任务时,任务就得进行排队等待执行,只能等待自己的前一个任务执行完成后自己才能执行,但是js可以借助浏览器和node环境实现多线程,即js执行是单线程的,但是js可以实现多线程。
2、事件循环就是js实现多线程,处理异步编程的一个机制,事件循环就是执行宏任务队列和微任务队列中的宏任务和微任务,当执行宏任务队列中的一个宏任务完毕后,马上执行该宏任务执行过程中产生的微任务(如果产生有),执行完该宏任务后以及该宏任务对应的微任务后,就是这一轮事件循环结束,然后继续执行宏任务队列中的其他宏任务,继续开启事件循环,直到宏任务队列为空。
参考文章:
阿里一面:熟悉事件循环?那谈谈为什么会分为宏任务和微任务。 - 掘金
https://www.jb51.net/article/223869.htm