深扒JS事件循环机制

js是一个单线程的语言,这源于Brendan Eich在发明js的时候觉得一个浏览器的脚本语言不用弄得那么复杂,这位老哥只花了十天就弄出了javascript,我想他应该怎么也没想到js会红到这地步,今天就围绕单线程这个特性说一说Node的几个计时器还有事件循环机制。

宏任务和微任务

js既然是单线程,那么任务多起来的时候就必须排队,那么怎么排就是第一个问题。首先这里简单解释一下宏任务和微任务,之前看到有的文章说什么微任务会在宏任务执行前执行,纯粹的胡扯,宏任务和微任务的区别就好比你去银行排队办业务,柜台就是单线程,你拿到的号就是一个宏任务,当轮到你的时候,就是相当于正在执行你这个宏任务,那么这时候,你可以存钱,办卡,再整点金融业务啥的,这些单个小的任务就是微任务,因为柜台小姐姐不会因为你存钱的同时又要办卡而去叫你再拿一个号,所以,在当前的微任务没有执行完成时,是不会执行下一个宏任务的。 看看下面的代码

// 异步宏任务
setTimeout(_ => console.log('你的后一个号'))

new Promise(resolve => {
  // 这里面的代码其实还是同步的,then里面的才是微任务
  resolve()
  console.log('你的号码')
}).then(_ => {
  console.log('存点钱')
}).then(_ => {
  console.log('办张卡')
}).then(_ => {
  console.log('转个帐')
}).then(_ => {
  console.log('搞点理财')
})
// 运行结果如下:
// 你的号码
// 存点钱
// 办张卡
// 转个帐
// 搞点理财
// 你的后一个号

这里列一下哪些些是宏任务哪些是微任务。

  • 宏任务:正常的异步任务都是宏任务,最常见的就是定时器(setInterval, setImmediate, setTimeout)、IO任务
  • 微任务:微任务出现比较晚,queueMicrotask、Promise和async属于微任务(当然,async就是promise)

事件循环概念

贴一下官方文档

“When Node.js starts, it initializes the event loop, processes the provided input script which may make async API calls, schedule timers, or call process.nextTick(), then begins processing the event loop.”
原文链接

js在开始执行之后会先把同步的代码执行完毕,这里的同步代码包括发起异步请求,把异步请求的回调塞到任务队列去,当同步代码执行完,也就是执行栈空了的时候,就会去任务队列找活干,以此不停循环,这个就是事件循环的简单概念。

事件循环再挖深一层

前面说了事件循环的概念,但是一轮事件循环也会分为6个阶段,并且这些阶段会依次执行。每个阶段都有一个先进先出的回调函数队列。只有一个阶段的回调函数队列清空了,该执行的回调函数都执行了,事件循环才会进入下一个阶段。

  1. timers
  2. I/O callbacks
  3. idle, prepare (与js无关)
  4. poll
  5. check
  6. close callbacks

timer阶段

这个阶段就是定时器执行阶段,到了这个阶段之后线程会看看是否有满足条件的定时器,简单来说就是到了应该执行的定时器,我们都知道setTimeout和setInterval第二个参数是延迟执行的毫秒数,这里的这个毫秒数其实是不准确的,真实的执行延迟肯定会比那个毫秒数大,理论上那个毫秒数是不能设置为0的,当你设置为0或者不设置时,js会默认地给你设成1。

I/O callbacks阶段

这个就是检查有没有准备好的输入输出了,有的话就执行其callback

Poll阶段

等待I/O阶段

check阶段

执行setImmediate的回调

close callbacks

一些关闭请求的回调函数执行

本轮循环和次轮循环

上一小节说了单轮循环的情况,代码的执行其实还涉及到本轮循环和次轮循环的问题。首先看看那几个异步的操作是按照怎样的规则进入任务队列的。Node 规定,process.nextTick和Promise的回调函数,追加在本轮循环,即同步任务一旦执行完成,就开始执行它们。而setTimeout、setInterval、setImmediate的回调函数,追加在次轮循环。简单看看代码:

console.log('本轮循环')
process.nextTick(()=>{
  console.log('本轮循环最先执行的nextTick')
})
Promise.resolve().then(()=>{
  console.log('比nextTick慢的Promise回调')
})

setTimeout(()=>{
  console.log('次轮setTimeout')
})
const interval = setInterval(()=>{
  clearInterval(interval)
  console.log('次轮setInterval')
},1000)
setImmediate(()=>{
  console.log('次轮setImmediate')
})
// 本轮循环
// 本轮循环最先执行的nextTick
// 比nextTick慢的Promise回调
// 次轮setTimeout
// 次轮setImmediate
// 次轮setInterval

上面的代码里面setImmediate和setTimeout这两个很魔性,执行顺序是不一定的,谁执行先都说不定。

setImmediate和setTimeout

按上面常理分析同一个事件循环内setTimeout是timer阶段而setImmediate是check阶段,所以应该是setTimeout执行会比setImmediate快,但是其实setTimeout的执行是不能设置为0ms,那第二个参数的取值范围在1~2147483647之间,那么这两个函数相邻执行的时候哪个执行先就是看运气的事情了。如果在timer阶段已经过了1毫秒那setTimeout就到了可以执行的时机,那么setTimeout就会先执行,如果没到的话timer阶段就会跳过,一直到check阶段先执行setImmediate,这就是这两个函数执行时机不确定的原因。但世事无绝对,上面说的只是一般的情况,下面有个有趣的例子。下面这个例子代码就一定是先输出setImmediate再输出setTimeout。

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

简单分析一下,首先上面的代码读取文件,这个属于IO操作,也就是程序在读取完文件后会运行到I/O callbacks阶段,这时候timer阶段已经过了,就执行到了check阶段,执行到check阶段的时候发现有个console.log(‘setImmediate’),于是就执行了。所以setImmediate会先输出。

总体理解事件循环机制

下面结合代码分析一下js的执行过程

const fs = require('fs');

console.log('开始')

const timeoutScheduled = Date.now();
setTimeout(() => {
  const delay = Date.now() - timeoutScheduled;
  console.log(`setTimeout:${delay}ms`);
}, 100);

fs.readFile('test.js', () => {
  const startCallback = Date.now();
  console.log("读取时间:", startCallback - timeoutScheduled)
  console.log("读取完毕,开始等500ms")
  while (Date.now() - startCallback < 500) {
    // 什么也不做
  }
});

// nextTick
process.nextTick(() => {
  console.log("nextTick")
})

// setImmediate
setImmediate(() => {
  console.log('setImmediate')
})

// promise
Promise.resolve().then(() => {
  console.log("Promise")
})

// 开始
// nextTick
// Promise
// setImmediate
// 读取时间: 2
// 读取完毕,开始等500ms
// setTimeout:502ms

执行顺序
同步执行部分:

  1. fs定义
  2. console.log(‘开始’)
  3. 执行到setTimeout,把他的回调函数扔到宏任务队列中去
  4. 执行到fs的IO操作,回调扔到宏任务去
  5. 执行到nextTick,回调扔到单独的一个队列,因为nextTick的操作会在所有的同步代码执行之后执行
  6. 执行到setImmediate,回调扔到宏任务去
  7. 执行到Promise,回调扔到微任务去

异步执行部分:

  1. 执行nextTick,nextTick是最先执行的异步任务
  2. 去微任务队列中去找,发现了个Promise的回调,执行输出Promise
  3. 去到宏任务队列找,经过timer阶段,看到第一个setTimeout还没到时间执行,忽略setTimeout,其实这时候文件也还没读取完,中间有一段等待的时间,直到文件被读取完,一般小的文件不会超过100ms,进入到IO回调阶段,这个阶段执行输出了读取时间,读取的时间花了2ms,这里注意,这文件读取回调里面写了个等待500ms的操作,而且是同步的等待,在这个while执行完之前是不会跳出IO阶段的,即使在这个IO阶段setTimeout已经到了执行时机也不会执行,过了500ms之后,IO阶段终于走完,然后进行新的一轮循环,进到timer阶段之后发现有个setTimeout一直在等着,最后就执行了setTimeout,执行输出。

这上面的执行过程中值得注意的就是setTimeout,虽然我们给他设置了个100ms,但是因为在IO回调阶段拖了很久,这个setTimeout并不能跟我们设置的一样在100ms之后输出,而是在502ms之后输出。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值