浏览器和 Node 中的事件循环 Event Loop 对比

什么是 Event Loop ?

众所周知,JavaScript 是非阻塞单线程语言,在浏览器执行过程中会遇到很多事件,而这些事件的执行就涉及到异步处理(包括数据请求 Ajax 、用户交互事件、脚本、渲染等)这么多的事件肯定得有个执行顺序,这就涉及到 Event Loop 。

而浏览器端和 node 的事件循环又有所不同。

浏览器

JavaScript 中分为执行栈和任务队列,而任务队列又分为微任务( MicroTask )与宏任务( MacroTask )

MicroTask:Promise、MutationObserve、Process.nextTick( node 才有)

MacroTask:setTimeout、setInterval、IO回调、页面交互事件、setImmediate( node )、MessageChannel

注意:在浏览器中,执行优先级为 同步代码 > 微任务 > 宏任务

现在我们讲讲执行栈和任务队列的事情,在浏览器中,首先会将同步代码加入执行栈一次弹出执行,当在执行过程中,遇到异步事件不会一直等待到事件完成,而是将异步任务挂起,继续执行同步代码,当异步任务返回结果时,将异步任务的回调事件加入到一个事件队列当中去,这个事件队列里的任务并不会立即执行,而是等同步任务全部执行完,再依次执行事件队列里的事件,在这个过程中,会优先取出微任务(MicroTask),然后取出宏任务(MacroTask)

注意:微任务队列会一次性全部执行清空

举个栗子:

console.log('script start');
setTimeout(function () {
    console.log('setTimeout');
}, 0);
new Promise((resolve,reject)=>{
    console.log('promise');
    resolve();
}).then(function () {
    console.log('promise1');
}).then(function () {
    console.log('promise2');
}).then(function () {
    console.log('promise3');
});
console.log('script end');

首先将同步代码推入执行栈执行,首先输出 'script start' ,然后将setTimeout挂起推入宏任务队列,这里注意 new Promise() 中的代码是同步代码,会直接执行,输出 'promise' ,然后将 then 里的回调函数推入微任务队列,事件循环,检查微任务队列,此时已经有 Promise 的回调函数,推入执行栈,输出 'promise1' 。Promise还有回调函数,推入微任务事件队列,执行栈结束。Promise 还有回调函数,推入微任务事件队列,执行栈结束,重复事件循环,依次输出 'promise2' , 'promise3' ,继续循环,此时微任务队列没有任务,检查宏任务队列,将 setTimeout 推入执行栈,输出 'setTimeout' ,至此,事件循环结束。

Node

                           <------microTasks
   ┌───────────────────────┐
┌─>│        timers         │
│  └──────────┬────────────┘
|             |            <----- microTasks
│  ┌──────────┴────────────┐
│  │     I/O callbacks     │
│  └──────────┬────────────┘
|             |            <----- microTasks
│  ┌──────────┴────────────┐
│  │     idle, prepare     │
│  └──────────┬────────────┘ 
|             |            <----- microTasks
|             |
|             |                   ┌───────────────┐
│  ┌──────────┴────────────┐      │   incoming:   │
│  │         poll          │<─────┤  connections, │
│  └──────────┬────────────┘      │   data, etc.  │
|             |                   └───────────────┘
|             |            <----- microTasks
│  ┌──────────┴────────────┐      
│  │        check          │
│  └──────────┬────────────┘
|             |            <----- microTasks
│  ┌──────────┴────────────┐
└──┤    close callbacks    │
   └───────────────────────┘

当 Node 启动时,回初始化 Event Loop ,每个 Loop 都有个阶段

  • timers 阶段:执行 setTimeout、setInterval 的 callback 回调。
  • I/O callbacks阶段:执行除了 close 事件的 callbacks 、被timers(定时器,setTimeout、setInterval 等)设定的 callbacks 、setImmediate() 设定的 callbacks 之外的 callbacks 
  • idle,prepare阶段:node 内部使用,Process.nextTick 在此阶段执行
  • poll 阶段:获取新的 I/O 事件, 适当的条件下 node 将阻塞在这里
  • check 阶段:执行 setImmediate() 的回调函数
  • close callbacks 阶段:执行 close 事件的 callback ,例如 socket.on('close', callback);

(2018/9/20 更新)

poll 阶段有两个主要功能:执行下限时间已经达到的 timers 的回调,然后处理 poll 队列里的事件。
当 event loop 进入 poll 阶段,并且 没有设定的 timers(there are no timers scheduled),会发生下面两件事之一:

如果 poll 队列不空,event loop 会遍历队列并同步执行回调,直到队列清空或执行的回调数到达系统上限;

如果 poll 队列为空,则发生以下两件事之一:

如果代码已经被 setImmediate() 设定了回调,event loop 将结束 poll 阶段进入 check 阶段来执行 check 队列里的回调。

如果代码没有被 setImmediate() 设定回调,event loop 将阻塞在该阶段等待回调被加入 poll 队列,并立即执行。

但是,当 event loop 进入 poll 阶段,并且 有设定的 timers ,一旦 poll 队列为空( poll 阶段空闲状态):

event loop 将检查timers,如果有 1 个或多个 timers 的下限时间已经到达,event loop将绕回 timers 阶段,并执行 timer 队列。

 

注意:在 node 中,微任务不在 Event Loop 的任何阶段执行,而是在各个阶段切换的中间执行,即从一个阶段切换到下个阶段前执行

在别处看到的栗子:

setTimeout(function timeout () {
  console.log('timeout');
},0);

setImmediate(function immediate () {
  console.log('immediate');
});

这段代码在 node 中的输出是不定的,因为 Event Loop 准备需要时间,setTimeout有最小毫秒数的,通常是4ms,如果准备时间大于 setTimeout 最小毫秒数,则此时 timer 阶段已经有 setTimeout 宏任务,先执行 setTimeout ,然后到 check 阶段执行 setImmediate,反之也有可能。

在看一个栗子:

setTimeout(()=>{
    console.log('timer1')
 
    Promise.resolve().then(function() {
        console.log('promise1')
    })
}, 0)
 
setTimeout(()=>{
    console.log('timer2')
 
    Promise.resolve().then(function() {
        console.log('promise2')
    })
}, 0)

在浏览器中输出:timer1 =》 promise1 =》timer2 =》promise2

在node中输出:timer1 =》timer2 =》promise1 =》promise2

总结:

前端真的不能停。

参考文章:

浏览器和Node.js中的Event Loop

【Node.js】理解事件循环机制

发布了60 篇原创文章 · 获赞 17 · 访问量 4万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 编程工作室 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览