【Node】EventLoop(时间循环)详解
一、什么是EventLoop?
想要了解event loop我们就要从js的工作原理说起。首先,大家都知道js是单线程的。所谓单线程就是进程中只有一个线程在运行。那么,js为什么是单线程而不是做成多线程的呢?个人理解,js是用来实现浏览器与用户之间的交互的。如果同时要处理用户点击,用户输入,用户关闭等操作,浏览器无法知道这个时间我到底应该做什么。所以js是从上至下按顺序运行下去的
按照单线程的思想,顺序执行我们的代码,那么,如果我们的js中间向后台发送一个ajax请求,就要等到请求等到结果后才会继续向下执行。如果请求耗时10秒,页面就要停在这里10秒。这样的用户体验很不好。。。因此,就有了同步任务、异步任务的区别
同步任务和异步任务在js中是如何执行的呢?js的代码运行会形成一个主线程和一个任务队列。主线程会从上到下一步步执行我们的js代码,形成一个执行栈。同步任务就会被放到这个执行栈中依次执行。而异步任务被放入到任务队列中执行,执行完就会在任务队列中打一个标记,形成一个对应的事件。当执行栈中的任务全部运行完毕,js会去提取并执行任务队列中的事件。这个过程是循环进行的,这就是我们今天想要了解的event loop
因为js的event loop机制,所以大家不要认为setTimeOut设置的事件到了延迟时间就是被执行。如果你的执行栈任务没有被全部执行完,清空。setTimeOut事件执行的时间很有可能是要大于你设置的延时参数
二、宏任务和微任务
异步任务之间是有执行优先级的区别的。不同的异步任务会被分为两类,微任务和宏任务
宏任务: 需要多次事件循环才能执行完,事件队列中的每一个事件都是一个宏任务。浏览器为了能够使得js内部宏任务与DOM任务有序的执行,会在一个宏任务执行结束后,在下一个宏执行开始前,对页面进行重新渲染 (task->渲染->task->…)鼠标点击会触发一个事件回调,需要执行一个宏任务,然后解析HTML
微任务: 微任务是一次性执行完的。微任务通常来说是需要在当前task执行结束后立即执行的任务,例如对一些动作做出反馈或者异步执行任务又不需要分配一个新的task,这样便可以提高一些性能。只要执行栈中没有其他的js代码正在执行了,而且每个宏任务都执行完了,微任务队列会立即执行。如果在微任务执行期间微任务队列加入了新的微任务,会将新的微任务加入队列尾部,之后也会被执行。简单理解,宏任务在下一轮事件循环执行,微任务在本轮事件循环的所有任务结束后执行
在一个事件循环中,异步事件返回结果后会被放到一个任务队列中。然而,根据这个异步事件的类型,这个事件实际上会被对应的宏任务队列或者微任务队列中去。并且在当前执行栈为空的时候,主线程会 查看微任务队列是否有事件存在。如果不存在,那么再去宏任务队列中取出一个事件并把对应的回到加入当前执行栈;如果存在,则会依次执行队列中事件对应的回调,直到微任务队列为空,然后去宏任务队列中取出最前面的一个事件,把对应的回调加入当前执行栈...如此反复,进入循环
宏任务:
包括整个代码script( script中的代码都属于宏任务 )
setTimeout
setInterval
微任务:
promise.then()
process.nextTick
事件执行的顺序:
先执行宏任务,然后执行微任务
三、案例
console.log("script start"); setTimeout(function(){ console.log("setTimeout"); },0) newPromise(resolve=>{ console.log("promise start"); resolve(); }).then(function(){ console.log("promise1"); }).then(()=>{ console.log("promise2"); }) console.log("script end"); 注意promise不算是一个异步任务它是一个同步任务
console.log(1); setTimeout(()=>{ console.log(2); }) newPromise((resolve)=>{ console.log(4) resolve() }).then(()=>{ setTimeout(()=>{ console.log(5); }) }).then(()=>{ console.log(6) }) console.log(7) setTimeout(() => { console.log(5) • new Promise(resolve => { console.log(6) • setTimeout(() => { console.log(7) }) resolve() }).then(() => { console.log(8) }) }, 500) new Promise(resolve => { console.log(9) resolve() }).then(() => { console.log(10) • setTimeout(() => { console.log(11) }, 0) }) •console.log(12)
四、NodeJS中的EventLoop
其实nodejs与浏览器的区别,就是nodejs的 宏任务 分好几种,而这好几种又有不同的 任务队列,而不同的 任务队列 又有顺序区别,而 微任务是穿插在每一种【注意不是每一个!】宏任务 之间的
Timers 类型的宏任务队列
setTimeout()
setInterval
Check 类型的宏任务队列
setImmediate()
Close callback 类型的宏任务队列
socket.on(‘close’, () => {})
Poll 类型的宏任务队列
除了上面几种的其他所有回调
五、nodeJs 里面的微任务队列
process.nextTick()
Promise.then()
process.nextTick()
的优先级高于所有的微任务,每一次清空微任务列表的时候,都是先执行process.nextTick()
六、NodeJS中的EventLoop与浏览器的EventLoop之间的区别
浏览器
先执行 一个 宏任务,然后执行所有微任务…...循环往复
NodeJS
先执行一种 宏任务 在执行清空微任务 再一种 宏任务 在执行清空微任务 再一种 宏任务 在执行清空微任务 所有种类宏任务结束 … 循环往复
分析浏览器和NodeJS执行的顺序
setTimeout(() =>{ console.log(‘timer1’) Promise.resolve().then(() =>{ console.log(‘promise1’) }) }, 0) setTimeout(() =>{ console.log(‘timer2’) Promise.resolve().then(() =>{ console.log(‘promise2’) }) }, 0)
七、setTimeout && setImmediate执行顺序
Node 并不能保证 timers 在预设时间到了就会立即执行,因为 Node 对 timers 的过期检查不一定靠谱,它会受机器上其它运行程序影响,或者那个时间点主线程不空闲
虽然 setTimeout 延时为 0,但是一般情况 Node 把 0 会设置为 1ms,所以,当 Node 准备 event loop 的时间大于 1ms 时,进入 timers 阶段时,setTimeout 已经到期,则会先执行 setTimeout;反之,若进入 timers 阶段用时小于 1ms,setTimeout 尚未到期,则会错过 timers 阶段,先进入 check 阶段,而先执行 setImmediate
setTimeout(() =>{ console.log('timeout') }, 0) setImmediate(() =>{ console.log('immediate') })
有一种情况,它们两者的顺序是固定的
constfs=require('fs') fs.readFile('test.txt', () =>{ console.log('readFile') setTimeout(() =>{ console.log('timeout') }, 0) setImmediate(() =>{ console.log('immediate') }) })
此时 setTimeout 和 setImmediate 是写在 I/O callbacks 中的,这意味着,我们处于 poll 阶段,然后是 check 阶段,所以这时无论 setTimeout 到期多么迅速,都会先执行 setImmediate。本质上是因为,我们从 poll 阶段开始执行
总结
当 setTimeout() 和 setImmediate() 都写在 main 里面的时候 不一定谁先执行谁后执行
当 setTimeout() 和 setImmediate() 都写在一个 I/O 回调 或者说一个 poll 类型宏任务的回调里面的时候 一定是先执行 setImmediate() 后执行 setTimeout()
八、Poll 阶段的两个主要功能
setImmediate 的 queue 不为空,则进入 check 阶段,然后是 close callbacks 阶段……