事件循环和任务队列
如有错误欢迎指正,谢~
浏览器的Event Loop:目前JS的主要运行环境有两个,浏览器和Node.js。这两个环境的Event Loop还有点区别,我们会分开来讲。
关于js事件循环机制,经常会在面试中被问到,究竟是怎么回事呢,让我们了解了解。
众所周知js是单线程,浏览器能够很好的处理异步请求,这事为什么呢?
浏览器是多进程的,浏览器的每个tab标签都是独立的进程,其中浏览器渲染进程(l浏览器内核)属于浏览器多进程的一种,主要负责页面的渲染,脚本执行,事件处理等。
其中包含的线程有:GUI渲染线程(负责页面渲染,解析HTML,css 构成dom树),js引擎线程,事件触发线程,定时器触发线程,http请求线程等主要线程。
执行中的线程:
主线程:就是js引擎线程,这个线程只有一个,页面渲染和函数处理在这个主线程上面工作。
工作线程:与主线程分开的线程,处理文件读取,网络请求等异步事件
任务队列:
所有任务可以分为同步任务和异步任务,同步任务,就是立即执行的任务,同步任务一般会直接进入主线程执行,而异步任务就是异步执行的任务,像ajax请求,settimeout等函数是异步任务,异步任务会通过任务队列机制,先进先出来进行执行。先看一个图:
同步任务和异步任务会进入不同的执行环境,同步任务进入主线程,即主执行栈,异步任务进入任务队列,主线程的任务执行完毕为空,会去任务队列读取任务,进入主线程执行,上述过程不断重复,我们称为事件循环(Event Loop)。
在每次事件循环经历如下步骤:
1,在 此次循环中,选择最先进入队列的,如果有则执行一次。
2,检测是否有Miscrotasks ,如果不停的执行,直至微任务队列为空。
3,更新render.
4,主线程重复执行上面步骤。
macrotasks(宏任务)(就是我们平常说的任务队列)和microtasks(微任务)的划分:
macrotasks包括:script (整体代码),I/O,UI交互事件,setTimeout,setInterval,setImmediate(node.js环境独有)。
microtasks包括:Promises,Object.observe(已废弃),MutationObserver(h5新增),process.nextTick(node.js环境独有)。
对于async/await 说明一下执行顺序:
async 声明的函数的返回本质是promise,await用于async函数内部,在等待一个异步返回接口,await等待过程中会跳出async,让出线程(阻塞await后面的代码执行),去执行async函数同级的代码,而await后面的代码属于异步代码,放入宏观任务等待执行。
趁热看个例子:
1 async function async1() { 2 console.log("async1 start"); 3 await async2(); 4 console.log("async1 end"); 5 } 6 async function async2() { 7 console.log("async2"); 8 } 9 console.log("script start"); 10 setTimeout(function () { 11 console.log("settimeout"); 12 }, 0); 13 async1(); 14 new Promise(function (resolve) { 15 console.log("promise1"); 16 resolve() 17 }).then(function () { 18 console.log("promise2"); 19 }) 20 console.log("script end");
让我们分析一下:
首先执行栈执行同步任务,先输出script start,之后遇到settimeout把回调函数放到宏观任务队列等待执行,遇到async1调用,执行同步任务输出async1 start,遇到await 调用async2函数,直接输出async2,而await后面的async1 end 放到宏任务队列中等待执行,接下来直接运行new promise 中同步代码输出promise1,然后resove,遇到then,异步微任务,把then回调函数放到微任务队列等待执行,再输出同步任务script end,执行栈为空了。去查看微任务队列有没有等待执行的代码,一看有个then回调等待执行呢,输出promise2,执行完全部的微任务队列,进行下一次循环,执行宏任务队列,输出async1 end,再看微任务队列有没有任务,一看没有,进行下一次循环,再看宏观任务队列有没有任务,最后一个宏观任务,输出settimeout。执行完毕。
//最后结果
script start
async1 start
async2
promise1
script end
promise2
async1 end
settimeout
再来说一下微任务队列中还分两种优先级队列,其中就是process.nextTickt微任务队列和promise.then微任务队列,在执行微任务队列的时候,process.nextTick队列优先级高于promise.then队列,优先执行全部的process.nextTick,再执行promise.then。
再看一下例子:
1 async function async1() { 2 console.log("async1 start"); 3 await async2(); 4 console.log("async1 end"); 5 } 6 async function async2() { 7 console.log("async2"); 8 } 9 console.log("script start"); 10 setTimeout(function () { 11 console.log("settimeout"); 12 }, 0); 13 async1(); 14 new Promise(function (resolve) { 15 console.log("promise1"); 16 resolve() 17 }).then(function () { 18 console.log("promise2"); 19 }) 20 process.nextTick(function(){ 21 console.log('process nextTick1') 22 process.nextTick(function(){ 23 console.log('process nextTick2') 24 25 }) 26 }) 27 28 console.log("script end");
//最后结果
script start
async1 start
async2
promise1
script end
process nextTick1
process nextTick2
promise2
async1 end
settimeout
Node.js的Event Loop
所以与浏览器Event Loop也是不一样的。Node的Event Loop是分阶段的,如下图所示:
(1)timers: 执行setTimeout和setInterval的回调
(2)pending callbacks: 执行延迟到下一个循环迭代的 I/O 回调
(3)idle, prepare: 仅系统内部使用
(4)poll: 检索新的 I/O 事件;执行与 I/O 相关的回调。事实上除了其他几个阶段处理的事情,其他几乎所有的异步都在这个阶段处理。
(5)check: setImmediate在这里执行
(6)close callbacks: 一些关闭的回调函数,如:socket.on('close', ...)
还有个需要注意的是poll
阶段,他后面并不一定每次都是check
阶段,poll
队列执行完后,如果没有setImmediate
但是有定时器到期,他会绕回去执行定时器阶段
setImmediate
和setTimeout:
上面的这个流程说简单点就是在一个异步流程里,setImmediate
会比定时器先执行。
1 console.log('outer'); 2 3 setTimeout(() => { 4 setTimeout(() => { 5 console.log('setTimeout'); 6 }, 0); 7 setImmediate(() => { 8 console.log('setImmediate'); 9 }); 10 }, 0);
//运行结果:
outer
setImmediate
setTimeout
(1)外层是一个setTimeout,所以执行他的回调的时候已经在timers阶段了
(2)处理里面的setTimeout,因为本次循环的timers正在执行,所以他的回调其实加到了下个timers阶段
(3)处理里面的setImmediate,将它的回调加入check阶段的队列
(4)外层timers阶段执行完,进入pending callbacks,idle, prepare,poll,这几个队列都是空的,所以继续往下
(5)到了check阶段,发现了setImmediate的回调,拿出来执行
(6)然后是close callbacks,队列时空的,跳过
(7)又是timers阶段,执行我们的console
但是请注意我们上面console.log('setTimeout')
和console.log('setImmediate')
都包在了一个setTimeout
里面,如果直接写在最外层会怎么样呢?代码改写如下:
console.log('outer'); setTimeout(() => { console.log('setTimeout'); }, 0); setImmediate(() => { console.log('setImmediate'); });
outer
setImmediate
setTimeout
有时是:
outer
setTimeout
setImmediate
我们顺着之前的Event Loop再来理一下。在理之前,需要告诉大家一件事情,
node.js里面setTimeout(fn, 0)会被强制改为setTimeout(fn, 1),这在官方文档中有说明。(说到这里顺便提下,HTML 5里面setTimeout最小的时间限制是4ms)。我们来理一下流程:
(1)外层同步代码一次性全部执行完,遇到异步API就塞到对应的阶段
(2)遇到setTimeout,虽然设置的是0毫秒触发,但是被node.js强制改为1毫秒,塞入times阶段
(3)遇到setImmediate塞入check阶段
(4)同步代码执行完毕,进入Event Loop
(5)先进入times阶段,检查当前时间过去了1毫秒没有,如果过了1毫秒,满足setTimeout条件,执行回调,如果没过1毫秒,跳过
(6)跳过空的阶段,进入check阶段,执行setImmediate回调
再来了解一下process.nextTick
先看一个例子:
1 process.nextTick(function(){
2 console.log('process nextTick1')
3 process.nextTick(function(){
4 console.log('process nextTick2')
5
6 })
7 })
8 setTimeout(()=>{
9 console.log('timout')
10 },0)
//结果是:
process nextTick1
process nextTick2
timout
由于process.nextTick方法指定的回调函数,总是在执行栈的尾部触发,如果有多个process.nextTick语句(不管是否嵌套),将全部在当前’执行栈‘执行。
掌握了概念就看例子吧:
1 setTimeout(function(){ 2 setImmediate(function(){ 3 console.log('setImmediate2') 4 }) 5 setTimeout(()=>{ 6 console.log('timeout') 7 },0) 8 },0)
setImmediate2
timeout
1 setTimeout(function(){ 2 // console.log('setImmediate1') 3 setImmediate(function(){ 4 console.log('setImmediate2') 5 setImmediate(function(){ 6 console.log('setImmediate3') 7 }) 8 }) 9 setTimeout(()=>{ 10 console.log('timeout') 11 },0) 12 },0)
setImmediate2
timeout
setImmediate3
1 setTimeout(function(){ 2 console.log(1) 3 },0); 4 new Promise(function(resolve){ 5 console.log(2) 6 for( var i=100000 ; i>0 ; i-- ){ 7 i==1 && resolve() 8 } 9 console.log(3) 10 }).then(function(){ 11 console.log(4) 12 }); 13 console.log(5);
结果是
2,3,5,4,1
eg2:
1 setTimeout(function(){ 2 console.log(1) 3 },0); 4 setTimeout(function(){ 5 console.log(6) 6 },2000); 7 new Promise(function(resolve){ 8 console.log(2) 9 for( var i=100000 ; i>0 ; i-- ){ 10 i==1 && resolve() 11 } 12 console.log(3) 13 }).then(function(){ 14 console.log(4); 15 setTimeout(function(){ 16 console.log(8) 17 },1000) 18 }); 19 console.log(5);
这个执行结果是啥呢???想想
结果是:
2
3
5
4
1
8
6
eg3:
setImmediate(function(){ console.log(1); },0); setTimeout(function(){ console.log(2); },0); new Promise(function(resolve){ console.log(3); resolve(); console.log(4); }).then(function(){ console.log(5); }); console.log(6); process.nextTick(function(){ console.log(7); }); console.log(8);
node 环境下:
3
4
6
8
7
5
2
1
eg4:
//加入两个nextTick的回调函数 process.nextTick(function () { console.log('nextTick延迟执行1'); }); process.nextTick(function () { console.log('nextTick延迟执行2'); }); // 加入两个setImmediate()的回调函数 setImmediate(function () { console.log('setImmediate延迟执行1'); // 进入下次循环 process.nextTick(function () { console.log('强势插入'); }); }); setImmediate(function () { console.log('setImmediate延迟执行2'); }); console.log('正常执行');
node环境:
正常执行
nextTick延迟执行1
nextTick延迟执行2
setImmediate延迟执行1
setImmediate延迟执行2
强势插入
总结一下,便于自己学习,也分享大家一起学习。(#^.^#)