最近在看js事件循环,事件循环是js运行的核心,js 是单线程的, js 的异步事件就是依赖于事件循环机制,网上找了些资料,发现腾讯云这篇 js事件循环 写的很详细,下文基于这一篇文章,外加上本身的一些总结。javascript
首先,咱们来解释下事件循环是个什么东西: 就咱们所知,浏览器的js是单线程的,也就是说,在同一时刻,最多也只有一个代码段在执行,但是浏览器又能很好的处理异步请求,那么究竟是为何呢?html
关于执行中的线程:前端
咱们来看一张图(这张图来自于http://www.zcfy.cc/article/node-js-at-scale-understanding-the-node-js-event-loop-risingstack-1652.html)java
从上图咱们能够看出,js主线程它是有一个执行栈的,全部的js代码都会在执行栈里运行。咱们看看浏览器上的执行栈node
在执行代码过程当中,若是遇到一些异步代码(好比setTimeout,ajax,promise.then以及用户点击等操做),那么浏览器就会将这些代码放到另外一个线程(在这里咱们叫作幕后线程)中去执行,在前端由浏览器底层执行,在 node 端由 libuv 执行,这个线程的执行不阻塞主线程的执行,主线程继续执行栈中剩余的代码。ajax
当幕后线程(background thread)里的代码执行完成后(好比setTimeout时间到了,ajax请求获得响应),该线程就会将它的回调函数放到任务队列(又称做事件队列、消息队列)中等待执行。而当主线程执行完栈中的全部代码后,它就会检查任务队列是否有任务要执行,若是有任务要执行的话,那么就将该任务放到执行栈中执行。若是当前任务队列为空的话,它就会一直循环等待任务到来。所以,这叫作事件循环。chrome
那么,问题来了。若是任务队列中,有不少个任务的话,那么要先执行哪个任务呢? 其实(正如上图所示),js是有两个任务队列的,一个叫作 Macrotask Queue(Task Queue) 大任务, 一个叫作 Microtask Queue 小任务promise
Macrotask 常见的任务:浏览器
Microtask 常见的任务:网络
那么,二者有什么具体的区别呢?或者说,若是两种任务同时出现的话,应该选择哪个呢?
其实事件循环执行流程以下:
简而言之,一次事件循环只执行处于 Macrotask 队首的任务,执行完成后,当即执行 Microtask 队列中的全部任务。
咱们先来看一段代码
console.log(1)
setTimeout(function() {
//settimeout1
console.log(2)
}, 0);
const intervalId = setInterval(function() {
//setinterval1
console.log(3)
}, 0)
setTimeout(function() {
//settimeout2
console.log(10)
new Promise(function(resolve) {
//promise1
console.log(11)
resolve()
})
.then(function() {
console.log(12)
})
.then(function() {
console.log(13)
clearInterval(intervalId)
})
}, 0);
//promise2
Promise.resolve()
.then(function() {
console.log(7)
})
.then(function() {
console.log(8)
})
console.log(9)
复制代码
你以为结果应该是什么呢? 我在node环境和chrome控制台输出的结果以下:
1
9
7
8
2
3
10
11
12
13
复制代码
在上面的例子中
从macrotask队列里取位于队首的任务(settimeout1)并执行,输出2 microtask队列为空,回到第一步,进入下一个事件循环,此时macrotask队列为: setinterval1,settimeout2
从macrotask队列里取位于队首的任务(setinterval1)并执行,输出3,而后又将新生成的setinterval1加入macrotask队列 microtask队列为空,回到第一步,进入下一个事件循环,此时macrotask队列为: settimeout2,setinterval1
从macrotask队列里取位于队首的任务(settimeout2)并执行,输出10,而且执行new Promise内的函数(new Promise内的函数是同步操做,并非异步操做),输出11,而且将它的两个then函数加入microtask队列 从microtask队列中,取队首的任务执行,直到为空为止。所以,两个新增的microtask任务按顺序执行,输出12和13,而且将setinterval1清空。
此时,microtask队列和macrotask队列都为空,浏览器会一直检查队列是否为空,等待新的任务加入队列。 在这里,你们能够会想,在第一次循环中,为何不是macrotask先执行?由于按照流程的话,不该该是先检查macrotask队列是否为空,再检查microtask队列吗?
缘由:由于一开始js主线程中跑的任务就是macrotask任务
,而根据事件循环的流程,一次事件循环只会执行一个macrotask任务,所以,执行完主线程的代码后,它就去从microtask队列里取队首任务来执行。
注意: 因为在执行microtask任务的时候,只有当microtask队列为空的时候,它才会进入下一个事件循环,所以,若是它源源不断地产生新的microtask任务,就会致使主线程一直在执行microtask任务,而没有办法执行macrotask任务,这样咱们就没法进行UI渲染/IO操做/ajax请求了,所以,咱们应该避免这种状况发生。在nodejs里的process.nexttick里,就能够设置最大的调用次数,以此来防止阻塞主线程。
以此,咱们来引入一个新的问题,定时器的问题。定时器是不是真实可靠的呢?好比我执行一个命令:setTimeout(task, 100),他是否就能准确的在100毫秒后执行呢?其实根据以上的讨论,咱们就能够得知,这是不可能的。
咱们看这个栗子
const s = new Date().getSeconds();
setTimeout(function() {
// 输出 "2",表示回调函数并无在 500 毫秒以后当即执行
console.log("Ran after " + (new Date().getSeconds() - s) + " seconds");
}, 500);
while(true) {
if(new Date().getSeconds() - s >= 2) {
console.log("Good, looped for 2 seconds");
break;
}
}
复制代码
若是不知道事件循环机制,那么想固然就认为 setTimeout 中的事件会在 500 毫秒后执行,但其实是在 2 秒后才执行,缘由你们应该都知道了,主线程一直有任务在执行,直到 2 秒后,主线程中的任务才执行完成,这才去执行 macrotask 中的 setTimeout 回调任务。
由于你执行 setTimeout(task,100) 后,其实只是确保这个任务,会在100毫秒后进入macrotask队列,但并不意味着他能马上运行,可能当前主线程正在进行一个耗时的操做,也可能目前microtask队列有不少个任务,因此用 setTimeout 做为倒计时其实并不会保证准确。
关于 js 阻塞仍是非阻塞的问题,我以为能够这么理解,不够在这以前,咱们先理解下同步、异步、阻塞仍是非阻塞的解释,在网上看到一段描述的很是好,引用下
同步阻塞:小明一直盯着下载进度条,到 100% 的时候就完成。
同步非阻塞:小明提交下载任务后就去干别的,每过一段时间就去瞄一眼进度条,看到 100% 就完成。(轮询)
异步阻塞:小明换了个有下载完成通知功能的软件,下载完成就“叮”一声。不太小明仍然一直等待“叮”的声音(看起来很傻,不是吗最蠢)
异步非阻塞:仍然是那个会“叮”一声的下载软件,小明提交下载任务后就去干别的,听到“叮”的一声就知道完成了。(最机智)
咱们的解释:
while (true) {
if (new Date().getSeconds() - s >= 2) {
console.log("Good, looped for 2 seconds");
break;
}
}
console.log('end')
复制代码
console.log('end')
的执行须要在 while 循环结束后才能执行,若是循环一直没结束,那么线程就被阻塞了。