事件循环个人理解

本文通过看阮一峰大神博客后模拟总结,原文链接(http://www.ruanyifeng.com/blog/2018/02/node-event-loop.html)
javascript是一门单线程语言,javascript是一门单线程语言,javascript是一门单线程语言,重要的事情说三遍。

接下来我们就来看一下js的执行顺序:

在这里插入图片描述

  • 同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入Event Table并注册函数。
  • 当指定的事情完成时,Event Table会将这个函数移入Event Queue。
  • 主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行。
  • 上述过程会不断重复,也就是常说的Event Loop(事件循环)。

除了广义的同步任务和异步任务,我们对任务有更精细的定义:

  • macro-task(宏任务):包括整体代码script,setTimeout,setInterval
  • micro-task(微任务):Promise,process.nextTick

不同类型的任务会进入对应的Event Queue,比如setTimeout和setInterval会进入相同的Event Queue。

事件循环的顺序,决定js代码的执行顺序。进入整体代码(宏任务)后,开始第一次循环。接着执行所有的微任务。然后再次从宏任务开始,找到其中一个任务队列执行完毕,再执行所有的微任务。

setTimeout(function() {
    console.log('setTimeout');
})

new Promise(function(resolve) {
    console.log('promise');
}).then(function() {
    console.log('then');
})

console.log('console');
  • 这段代码作为宏任务,进入主线程。
  • 先遇到setTimeout,那么将其回调函数注册后分发到宏任务Event Queue。(注册过程与上同,下文不再描述)
  • 接下来遇到了Promise,new Promise立即执行,then函数分发到微任务Event Queue。
  • 遇到console.log(),立即执行。
  • 好啦,整体代码script作为第一个宏任务执行结束,看看有哪些微任务?我们发现了then在微任务Event Queue里面,执行。
  • ok,第一轮事件循环结束了,我们开始第二轮循环,当然要从宏任务Event Queue开始。我们发现了宏任务Event Queue中setTimeout对应的回调函数,立即执行。
  • 结束。

事件循环,宏任务,微任务的关系如图所示:

在这里插入图片描述

单独讲一下setTimeout

用一个例子来说明:

let o = new Date().getTime()
let n;
setTimeout(() => {
  m = new Date().getTime()
  console.log("计时1:",m - o)
  sleep(2000)
  n = new Date().getTime()
  console.log("计时2:",n - o)
}, 1000)
sleep(3000)

function sleep(ms) {
  let oldDate = new Date().getTime()
  let newDate = oldDate
  while (newDate - oldDate < ms){
    newDate = new Date().getTime()
  }
}
//计时1: 3001
//计时2: 5006
  • setTimeout回调函数进入Event Table并注册,计时开始。
  • 执行sleep函数,很慢,非常慢,计时仍在继续。
  • 1秒到了,计时事件timeout完成,setTimeout回调进入Event Queue,但是sleep也太慢了吧,还没执行完,只好等着。
  • sleep终于执行完了,setTimeout回调终于从Event Queue进入了主线程执行。

下边讲一下异步如何实现

客户端异步实现

我们经常在写客户端js的时候用到异步操作,实现异步基本上包含

  • setTimeout
  • setInterval
  • ajax

Node端异步实现

node中实现异步包含

  • setImmediate
  • process.nextTick

定时器用法差不多

看下边的例子:

setTimeout(()=>{
    console.log(1);
});
setImmediate(()=>{
    console.log(2);
});
process.nextTick(()=>{
    console.log(3);
});
Promise.resolve().then(()=>{
    console.log(4);
});
(()=>console.log(5))();

输出结果如下:

5
3
4
1
2

下边来分析一下结果

同步和异步任务

同步任务要比异步任务更早执行,因此首先输出5

本轮循环和次轮循环

异步任务分为两种:

  • 追加在本轮循环的异步任务
  • 追加在次轮循环的异步任务

本轮循环任务要优先于次轮循环任务

Node规定,process.nextTick、Promise的回调函数,追加在本轮循环任务中,即同步任务完成后就开始执行。而setTimeout、setInterval、setImmediate的回调函数追加在次轮循环任务中。因此

// 下面两行,次轮循环执行
setTimeout(() => console.log(1));
setImmediate(() => console.log(2));
// 下面两行,本轮循环执行
process.nextTick(() => console.log(3));
Promise.resolve().then(() => console.log(4));

process.nextTick()

process.nextTick任务在本轮循环中执行,而且是所有异步任务里执行最快的。如果希望快速执行异步任务就使用此函数。

微任务

Promise对象的回调函数会进入异步任务里边的“微任务”队列,微任务队列追加在process.nextTick队列后边,也属于本轮循环。

微任务队列

注意:只有前一个队列全部清空后才会执行下一个队列。

看下边的例子:

process.nextTick(() => console.log(1));
Promise.resolve().then(() => console.log(2));
process.nextTick(() => console.log(3));
Promise.resolve().then(() => console.log(4));
// 1
// 3
// 2
// 4

全部process.nextTick的回调函数会早于Promise

事件循环的概念

js只有一个主线程,事件循环是在主线程上完成的。
Node开始执行脚本时,先对事件循环进行初始化,但这时还没有执行事件循环,先进行以下操作。

  1. 同步任务
  2. 发出异步请求
  3. 规划定时器生效时间
  4. 执行本轮循环任务等等

上边事情都干完之后才进行事件循环。

事件循环的六个阶段

事件循环是无限次的执行,只有当异步任务回调函数队列都清空了,才会停止执行。
每一轮循环分为以下六个阶段

  1. timers
  2. I/O callbacks
  3. idle,prepare
  4. poll
  5. check
  6. close callbacks

每个阶段都有一个先进先出的队列,只有该队列都清空或者该执行的回调函数都执行了,时间循环才进入下一个阶段。

这里写图片描述

(1) timers

这是一个定时器阶段,处理setTimeoutsetInterval的回调函数,进入这个阶段,主线程会检查一下当前时间,是否满足定时器条件。如果满足就执行回调函数,否则调到下一个阶段。

(2) I/O callbacks

除了以下操作的回调函数,其他的回调函数都在这个阶段执行。

  • setTimeout()和setInterval()的回调函数
  • setImmediate()的回调函数
  • 关于关闭请求的回调函数,例如socket.on(“close”,…)

本文中例子执行的是读取文件后的回调

(3) idle, prepare

该阶段只供 libuv 内部调用,这里可以忽略。

(4) Poll

这个阶段是轮询时间,用于等待未返回的I/O事件,比如服务器的回应,文件的读取,用户鼠标移动等等。
这个阶段的时间比较长,如果没有其他异步要处理(比如到期的定时器),会一直停留在这个阶段,等待I/O的响应。

(5) check

该阶段执行setImmediate()的回调函数。

(6) close callbacks

该阶段执行关闭请求的回调函数,比如socket.on(‘close’, …)。

这里写图片描述

事件循环的示例

分析一个官方案例:

const fs = require('fs');

const timeoutScheduled = Date.now();

// 异步任务一:100ms 后执行的定时器
setTimeout(() => {
  const delay = Date.now() - timeoutScheduled;
  console.log(`${delay}ms`);
}, 100);

// 异步任务二:文件读取后,有一个 200ms 的回调函数
fs.readFile('test.js', () => {
  const startCallback = Date.now();
  while (Date.now() - startCallback < 200) {
    // 什么也不做
  }
});

第一轮事件循环,没有到期的定时器,也没有可以进行的I/O回调操作,所以会键入Poll阶段,等待内核返回文件读取结果,由于文件过小,读取时间不会超过100ms,所以在定时器到期之前Poll阶段就会返回结果。继续往下执行。

第二轮事件循环,依然没有到期的定期器,因此会执行I/O的回调函数,进入I/O的callbacks阶段,执行fs.readFile的回调函数,这个函数需要200ms,也就是执行到一半,定时器就会到期,但是必须等到这个回调执行完之后才会离开这个阶段。

第三轮事件循环,已经有了到期的定时器,所以会在timers阶段执行定时器,最后输出大约200多毫秒。

setTimeout 和 setImmediate

由于setTimeout在timers阶段,setImmediate在check阶段,所以setTimeout会比setImmediate提前执行。

setTimeout(() => console.log(1));
setImmediate(() => console.log(2));

上边代码应该先输出1后输出2,但是结果却是不确定的。有时会先输出2。
这是因为setTimeout的第二个参数默认为0,但是Node做不到0毫秒,最少也需要1ms,官方文档规定,setTimeout第二个参数范围在1~2147483647毫秒之间。
实际执行的时候,进入循环后,有可能到了1ms,也有可能没到1ms,取决于当前的系统状况。如果没到1ms那么timers阶段就会被跳过,进入check阶段,先执行setImmediate回调。

一下代码肯定是先输出2再输出1。

const fs = require('fs');

fs.readFile('test.js', () => {
  setTimeout(() => console.log(1));
  setImmediate(() => console.log(2));
});

上边代码先进入I/O callbacks阶段,然后是check阶段,因此setImmediate会早于setTimeout执行。


参考资料:
1、Node 定时器详解
2、这一次,彻底弄懂 JavaScript 执行机制

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值