JS的事件循环机制恐怕是大多数前端开发者头顶上的一座大山之一,最近通过拜读两篇文档,对eventloop进行了深刻的理解;通过这篇文档对要点进行总结;
article1:
波神的这篇eventLoop文章适合反复重温,写的很透彻,比其他解读事件机制的文章更加清晰透彻
前端基础进阶(十四):深入核心,详解事件循环机制
article2:
这一篇从同步异步,微任务宏任务来详细解读。
setTimeout和setImmediate到底谁先执行,本文让你彻底理解Event Loop - _蒋鹏飞 - 博客园
同步异步
我们一直说JS是单线程的,其实指的是,js运行的主线程只有一个,但是整个运行环境并不是单线程的;
js除了主线程外,还有定时器线程,事件触发线程,异步HTTP请求线程;
在js主线程中,有一个唯一的事件循环 Event loop(先不说web worker),从同步异步角度分析,流程图大致如下:
![【EventLoop】问题一次搞定_第1张图片](http://img.e-com-net.com/image/info8/3a5add0170b6484d9b0c1e010d188035.png)
- 主线程每次执行时,先看看要执行的是同步任务,还是异步的API
- 同步任务就继续执行,一直执行完
- 遇到异步API就将它交给对应的异步线程,自己继续执行同步任务
- 异步线程执行异步API,执行完后,将异步回调事件放入事件队列上
- 主线程上的同步任务干完后就来事件队列看看有没有任务
- 主线程发现事件队列有任务,就取出里面的任务执行
- 主线程不断循环上述流程
微任务,宏任务
上面只是简化版的Eventloop,事件队列还会分为微任务,宏任务。
![【EventLoop】问题一次搞定_第2张图片](http://img.e-com-net.com/image/info8/2a9b8e3ac6cd4dc7b3d1d6f22e1536d4.jpg)
- Event Loop是唯一的,可以有一个或多个事件队列,但是只有一个微任务队列;
- 宏任务包括:script整体代码,settimeout,setInterval,setImmediate,rendering;
- 微任务包括:process.nextTick, Promise;
- setTimeout/Promise等我们称之为任务源。而进入任务队列的是他们指定的具体执行任务;
- 来自不同任务源的任务会进入到不同的任务队列。其中setTimeout与setInterval是同源的;
- 微任务队列全部执行完会重新渲染一次;
- 每个宏任务执行完都会重新渲染一次;
- requestAnimationFrame处于渲染阶段,不在微任务队列,也不在宏任务队列;
- 它从script(整体代码)开始第一次循环。之后全局上下文进入函数调用栈。直到调用栈清空(只剩全局),然后执行所有的micro-task。当所有可执行的micro-task执行完毕之后。循环再次从macro-task开始,找到其中一个任务队列执行完毕,然后再执行所有的micro-task,这样一直循环下去。
注意点
// setTimeout中的回调函数才是进入任务队列的任务
setTimeout(function() {
console.log('xxxx');
})
// 非常多的同学对于setTimeout的理解存在偏差。所以大概说一下误解:
// setTimeout作为一个任务分发器/任务源头,这个函数会立即执行,而它所要分发的任务,
// 也就是它的第一个参数,才是延迟执行
下面,通过一个复杂的栗子,我们来看一下,事件队列都是怎么运行的:
console.log('golb1');
setTimeout(function() {
console.log('timeout1');
process.nextTick(function() {
console.log('timeout1_nextTick');
})
new Promise(function(resolve) {
console.log('timeout1_promise');
resolve();
}).then(function() {
console.log('timeout1_then')
})
})
setImmediate(function() {
console.log('immediate1');
process.nextTick(function() {
console.log('immediate1_nextTick');
})
new Promise(function(resolve) {
console.log('immediate1_promise');
resolve();
}).then(function() {
console.log('immediate1_then')
})
})
process.nextTick(function() {
console.log('glob1_nextTick');
})
new Promise(function(resolve) {
console.log('glob1_promise');
resolve();
}).then(function() {
console.log('glob1_then')
})
setTimeout(function() {
console.log('timeout2');
process.nextTick(function() {
console.log('timeout2_nextTick');
})
new Promise(function(resolve) {
console.log('timeout2_promise');
resolve();
}).then(function() {
console.log('timeout2_then')
})
})
process.nextTick(function() {
console.log('glob2_nextTick');
})
new Promise(function(resolve) {
console.log('glob2_promise');
resolve();
}).then(function() {
console.log('glob2_then')
})
setImmediate(function() {
console.log('immediate2');
process.nextTick(function() {
console.log('immediate2_nextTick');
})
new Promise(function(resolve) {
console.log('immediate2_promise');
resolve();
}).then(function() {
console.log('immediate2_then')
})
}) |
第一步:宏任务script首先执行。全局入栈。glob1输出。 ![【EventLoop】问题一次搞定_第3张图片](http://img.e-com-net.com/image/info8/23644214da83468d9a5d92effd85e828.jpg) 第二步,执行过程遇到setTimeout。setTimeout作为任务分发器,将任务分发到对应的宏任务队列中。 ![【EventLoop】问题一次搞定_第4张图片](http://img.e-com-net.com/image/info8/4d7b1f6730b74fb4b904ca16c518813b.jpg) 第三步:执行过程遇到setImmediate。setImmediate也是一个宏任务分发器,将任务分发到对应的任务队列中。setImmediate的任务队列会在setTimeout队列的后面执行。 ![【EventLoop】问题一次搞定_第5张图片](http://img.e-com-net.com/image/info8/490974be7a6e491fa87e54daa01e2e3b.jpg) 第四步:执行遇到nextTick,process.nextTick是一个微任务分发器,它会将任务分发到对应的微任务队列中去。 ![【EventLoop】问题一次搞定_第6张图片](http://img.e-com-net.com/image/info8/cd11f5893b6d4f598d28f77a0bcd59a6.jpg) 第五步:执行遇到Promise。Promise的then方法会将任务分发到对应的微任务队列中,但是它构造函数中的方法会直接执行。因此,glob1_promise会第二个输出。 ![【EventLoop】问题一次搞定_第7张图片](http://img.e-com-net.com/image/info8/8a9c7811674a43ad8d641945ca66ed01.jpg) ![【EventLoop】问题一次搞定_第8张图片](http://img.e-com-net.com/image/info8/5726bc113337477eb81c01cb3df6280d.jpg) 第六步:执行遇到第二个setTimeout。 ![【EventLoop】问题一次搞定_第9张图片](http://img.e-com-net.com/image/info8/b42bf0cf64d741829ef49aa9f1729a33.jpg) 第七步:先后遇到nextTick与Promise ![【EventLoop】问题一次搞定_第10张图片](http://img.e-com-net.com/image/info8/351e61756c734b7c9cf73c76a1826d2b.jpg) 第八步:再次遇到setImmediate。 ![【EventLoop】问题一次搞定_第11张图片](http://img.e-com-net.com/image/info8/4eaefcf160d64fc2ab90320335fe291e.jpg) 第一轮循环结束,开始执行第一轮循环中所有的微任务队列 其中,nextTick队列会比Promie先执行。nextTick中的可执行任务执行完毕之后,才会开始执行Promise队列中的任务。 当所有可执行的微任务执行完毕之后,这一轮循环就表示结束了。下一轮循环继续从宏任务队列开始执行。 所以就从setTimeout队列开始执行 只有当setTimeout中所有的任务执行完毕之后,才会再次开始执行微任务队列。并且清空所有的可执行微任务。 setTiemout队列产生的微任务执行完毕之后,循环则回过头来开始执行setImmediate队列。仍然是先将setImmediate队列中的任务执行完毕,再执行所产生的微任务。 当setImmediate队列执行产生的微任务全部执行之后,第二轮循环也就结束了。 当我们在执行setTimeout任务中遇到setTimeout时,它仍然会将对应的任务分发到setTimeout队列中去,但是该任务就得等到下一轮事件循环执行了。例子中没有涉及到这么复杂的嵌套,大家可以动手添加或者修改他们的位置来感受一下循环的变化。 |
//执行第一轮宏任务队列(macro) golb1 glob1_promise glob2_promise //执行第一轮微任务队列(micro) glob1_nextTick glob2_nextTick glob1_then glob2_then 第一轮事件循环结束
//执行第二轮宏任务中的setTimeout队列(macro) timeout1 timeout1_promise timeout2 timeout2_promise //执行第二轮宏任务setTimeout产生的微任务队列(micro) timeout1_nextTick timeout2_nextTick timeout1_then timeout2_then 第二轮事件循环结束 //执行第二轮宏任务中setImmediate队列(macro) immediate1 immediate1_promise immediate2 immediate2_promise
//执行第二轮宏任务setImmediate产生的微任务队列(micro) immediate1_nextTick immediate2_nextTick immediate1_then immediate2_then |