nodejs的event是基于libuv,而浏览器的event loop则是在html5的规范中明确定义的
浏览器中的event loop - js异步执行的运行机制:
1.所有的任务都在主线程上执行,形成执行栈
2.主线程之外,还存在一个任务队列。只要异步任务有了运行结果,就会在任务队列中放置一个事件
3. 主线程的执行环境上首先执行同步任务,然后再依赖event loop机制来不断循环将任务队列中的各个task放到执行栈中(那些对应的异步任务,结束等待状态,进入执行栈并开始执行)。
4. 主线程不断重复以上三步
异步任务分为宏任务(macrotask)与微任务(microtask),不同的API注册的任务会依次进入自身对应的队列中,等待event loop将它们依次压入执行栈中执行
宏任务:script(整体代码),setTimeout,setTnterval,setImmediate,I/O
微任务:process.nextTick(),promise,Object.observe,MutationObserve等
事件循环的顺序是从script整体代码开始的第一次循环,随后全局上下文进入函数执行环境栈,碰到宏任务就将其交给处理它的模块处理完后将回调函数放入宏任务队列中,碰到微任务也将其处理后回调函数放入微任务队列中。直至函数执行环境栈中同步代码执行完毕,就会执行微任务,当所有的微任务执行完毕后,循环再次执行宏任务中的事件。不断循环。(task---micro-task---macro-task)
在碰到执行nwe Promise时,创建的Promise实例的时候,传入的函数将在执行环境栈中执行,回调函数then和catch放入微任务中
微任务中的全部的process.nextTick()优先于promise执行,是所有异步任务中运行最快执行的
nodejs中的event loop:
事件循环的六个阶段,每个阶段都有一个先进先出的回调函数队列,只有一个阶段的回调函数队列清空了,该执行的回调函数都执行了,事件循环才会进入下一个阶段。当所有阶段被顺序执行一次后,成event loop完成一个task
timers 阶段
这是定时器阶段,处理setTimeout()和setInterval()的回调函数。进入这阶段,主线程会检查一下当前事件,是否满足定时器的条件,满足就执行回调函数,否则离开这个阶段
I/O callback 阶段
除了setTimeout(),setIterval(),setTmmediate(),关闭请求的回调函数外,其他的回调函数都在这个阶段执行
idle,prepare 阶段
libuv内部调用,可忽略
poll 阶段
这个阶段是轮询时间,等待还未返回的I/O事件,如服务器的回应,用户移动鼠标等,接下来event loop会去检查有无预设的setImmediate(),若有预设的,将结束poll阶段进入check阶段,并执行check阶段的任务队列,若没有预设的,在poll阶段会检查timer队列是否为空,若为空,会一直等待I/O请求返回,若非空,event loop就开始下一轮事件循环,即重新进入timer阶段
check 阶段
该阶段执行setImmediate()的回调函数
close callbacks 阶段
该阶段执行关闭请求的回调函数
实例:
const date = Date.now();
//异步任务一:100ms后执行定时器
setTimeout(()=>{
const delay = Date.now() - data;
console.log(delay);
},100)
//异步任务二:文件读取后,有一个200ms的回调函数
fs.readFile(‘test.js’,()=>{
const startCallback = Data.now();
while(Data.now() - startCallback < 200){
//啥也不做
}
});
实例分析:
脚本进入第一轮事件循环后,没有到期的定时器,也没有可以执行的I/O回调函数,会直接进入poll阶段,等待内核返回读取文件的结果,由于读取小文件一般不回超过100ms,所有定时器到期之前,poll阶段得到结果,继续往下执行
第二轮事件循环,依然没有到期的定时器,但已经有了可以执行的I/O回调函数,进入I/O callbacks阶段,执行fs.readFile的回调函数,需要200ms,执行到一般的时候定时器就会到期,但必须等这个回调函数执行完毕,才会离开这个阶段
第三轮循环,已有了到期的定时器,所以会在timers阶段执行定时器,最后输出结果大概是200多ms
setTimeout和 setImmediate
setTimeout(()=>{console.log(1)})
setImmediate(()=>{console.log(2)}
正常情况,timers阶段在check阶段之前,因先输出1,再输出2。但因为setTimeout的第二个参数默认为0,实际上node请求时做不到0毫秒,官方文档表示第二参数的取值范围在1ms以上
实际执行的时候,进入实际循环以后,有可能耗费1ms,也可能还没到1ms,取决于系统状况。如果没到1ms,那么timers阶段会跳过,进入check阶段后,先执行了setImmediate的回调函数。
fs.readFile(‘test.js’,()=>{
setTimeout(()=>{console.log(1)})
setImmediate(()=>{console.log(2)}
})
上面代码进入了I/Ocallback阶段,接下来会先进入check阶段,再循环到timers阶段,所以会先执行2,再执行1