如何详细描述JS事件循环Event Loop

一、前言

面试官问:详细描述事件循环Event Loop?

这个时候并没有说是node的事件循环还是浏览器的事件循环,这个需要跟面试官确认一下,大多数是需要回答顺序:

  • 浏览器事件循环
  • Node事件循环
  • 一道输出值面试题

二、浏览器的事件循环

1、宏任务(Macro-Task)队列和微任务(Micro-Task)队列

浏览器事件循环中的异步队列有两种:macro(宏任务)队列和micro(微任务)队列。宏任务队列可以是多个,微任务队列只有一个。

  • 常见的macro-task:script代码块,setTimeout,setInterval,setImmediate,requestAnimationFrame,I/O操作,UI rendering渲染页面等。
  • 常见的micro-task:process.nextTick,MutationObserver监听(h5新特性),Promise.then,async/await,ajax,axios,catch finally,Object.observe(方法废弃)等。

2、详细过程:

  • 1、浏览器按照js的顺序加载script标签分隔的代码块。
  • 2、script代码块加载完毕,首先进行语法分析,一旦语法错误,就会跳出当前的script代码块。
  • 3、语法分析正确之后,立即进行预编译阶段。
  • 4、预编译阶段:1、创建变量对象(创建arguments对象,函数声明提前,变量声明提升);2、确定作用域链;3、this指向。
  • 5、然后进入执行阶段。
  • 6、当前执行栈为空,执行栈是一个函数调用的栈结构,先进后出的原则。微任务队列为空。宏任务队列只有一个script代码块。
  • 7、全局上下文被推入执行栈,同步代码执行。执行过程中,会判断同步还是异步,通过一些接口调用和定时器,I/O等,产生新的宏任务和微任务,然后分别推进各自的任务队列中。 同步代码执行完了,script代码块会被移出宏任务队列。这个过程就是队列的宏任务的执行和出队列过程。
  • 8、上一个出队是一个宏任务,这一步我们开始处理微任务。注意点:宏任务出队时,任务是一个一个执行,而微任务出队,任务是一队一队的执行。因此,我们开始处理微任务队列,会逐个执行队列中的任务,知道微任务队列被清空。
  • 9、执行渲染操作,更新页面。
  • 10、检查是否有web worker任务,如果有,对其处理。
  • 11、重复执行6--10过程,直到两个队列都被清空。
  • 12、重复执行1--11过程,直到所有代码块执行完毕。

三、Node的事件循环

1、简介

Node的事件循环和浏览器的事件循环是完全不同的东西。Node是采用v8作为js的解析引擎,而I/O处理也是使用自己设计的libuv库。 libuv是一个基于事件驱动的跨平台抽象层,封装了不同操作系统的一些底层特性,对外提供统一的接口API,事件循环机制也是libuv里面的实现。

Node的运行机制:

  • V8引擎解析JavaScript脚本。
  • 解析代码后,调用Node API。
  • libuv库负责Node API的执行。它将不同的任务分配给不同的线程,形成一个Event Loop(事件循环),以异步的方式将任务结果返回给V8引擎。
  • V8引擎再将结果返回给用户。

2、过程六个阶段

事件循环分为6个阶段,会按照顺序反复运行。每进入到某一个阶段,都会从对应的回调队列中取出函数去执行。 当队列为空或者执行的回调函数数量达到系统设定的阈值,就会进入到下一阶段。

(1)node事件循环的顺序:

  • 外部输入数据 --》轮询阶段(poll)--》检查阶段(check)--》关闭事件回调阶段(close callback)--》定时器检测阶段(timers)--》I/O事件回调阶段(I/O callbacks)--》闲置阶段(idle,prepare)--》轮询阶段(按照顺序反复运行)

说明:

  • timers阶段:这个阶段执行timer(setTimeout,setInterval等)回调。
  • I/O callbacks阶段:处理一些上一轮循环中的少数未执行的I/O回调。
  • idle,prepare阶段: 仅node内部使用。
  • poll阶段:获取新的I/O事件,适当条件下node将阻塞在这里。
  • check阶段:执行setImmediate()的回调。
  • close callbacks阶段:执行socket的close事件回调。

注意:6个阶段不包括process.nextTick()

日常开发中,主要是timers阶段,poll阶段,check阶段包含了绝大部分异步任务。

(2)定时器检测阶段-timers阶段

timers阶段会执行setTimeout和setInterval回调,并且由轮询poll阶段控制。同样,在Node中定时器指定的时间也不是准确时间,只能尽快执行。

(3)轮询阶段--poll阶段

轮询poll阶段是一个重要阶段,主要做两件事情:

  • 1、回到timers阶段执行回调
  • 2、执行I/O回调

进入到这个阶段如果没有设定timer的话,会做个判断:

  • 1、如果poll队列不为空,会遍历回调队列并同步执行,知道队列为空或者达到系统限制。
  • 2、如果队列为空,会做个判断:
    • 如果有setImmediate回调需要执行,poll阶段会停止并且进入到check阶段执行回调。
    • 如果没有setImmediate回调,会等待回调被加入到队列中并立即执行回调,这里会有个超时时间设置防止一直等待下去

当然设定了timer,并且poll队列为空,会判断是否有timer超时,如果有的话回到timer阶段执行回调。

(4)检查阶段--check阶段

setImmediate的回调会被加到check队列中,从EventLoop阶段图知道,check阶段的执行顺序是在poll阶段之后。

我们看个栗子,更容易让我们理解:

js 复制代码
console.log('start')
setTimeout(() => {
  console.log('timer1')
  Promise.resolve().then(function() {
    console.log('promise1')
  })
}, 0)
setTimeout(() => {
  console.log('timer2')
  Promise.resolve().then(function() {
    console.log('promise2')
  })
}, 0)
Promise.resolve().then(function() {
  console.log('promise3')
})
console.log('end')
// start, 
// end, 
// promise3,  
// timer1,  
// timer2,  
// promise1,  
// promise2 

分析:

  • 1、一开始执行栈的同步任务(宏任务),执行完毕后,依次打出 start和end,并将2个timer依次放入timers队列。
  • 2、然后去执行微任务(和浏览器有点像),打印出promise3。
  • 3、然后进入到timers阶段,执行timer1的回调函数,打印timer1,并将promise.then回调放入微任务队列,同样的步骤执行timer2,打印timer2。(这个和浏览器差别最大的地方),timers阶段有几个setTimeout/setInterval都会依次执行,并不像浏览器端,没执行一个宏任务后就去执行一个微任务队列。

3、node的微任务和宏任务

Node端事件循环中的异步队列也是这两种:macro(宏任务)队列和 micro(微任务)队列。

  • 1、常见的 macro-task 比如:setTimeout、setInterval、 setImmediate、script(整体代码)、 I/O 操作等。
  • 2、常见的 micro-task 比如: process.nextTick、new Promise().then(回调)等。

4、注意点

(1)setTimeout和setImmediate

二者比较相似,区别:调用的时机不同。

  • 1、setImmediate:设计在poll阶段完成时执行,也就是在check阶段执行。
  • 2、setTimeout:设计在poll阶段空闲的时候,设定的时间达到后执行,也就是在timer阶段执行。
js 复制代码
setTimeout(function timeout () {
    console.log('timeout');
},0);
setImmediate(function immediate () {
    console.log('immediate');
});

分析上述代码:

  • 执行之后,发现:setTimeout可能执行在前,也有可能执行在后。
  • 源码中,setTimeout(fn, 0) === setTimeout(fn, 1),进入事件循环也是需要成本的,如果在准备时候花费大于1ms,timer阶段就会直接执行setTimeout回调。
  • 如果准备时间花费小于1ms,那么就setImmediate回调先执行。

但是如果两者在异步I/O callback内部调用,总是先执行setImmediate,再执行setTimeout。

js 复制代码
const fs = require('fs')
fs.readFile(__filename, () => {
    setTimeout(() => {
        console.log('timeout');
    }, 0)
    setImmediate(() => {
        console.log('immediate');
    })
})
// immediate
// timeout

(2)process.nextTick

process.nextTick这个函数其实是独立于Event Loop之外的,有自己的队列。当每个阶段完成后,如果存在nextTick队列,就会清空队列中的所有回调函数,并且优先于其他microtask执行。

js 复制代码
Promise.resolve().then(function() {
    console.log('promise0')
})

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

process.nextTick(() => {
    console.log('nextTick')
    process.nextTick(() => {
        console.log('nextTick')
        process.nextTick(() => {
            console.log('nextTick')
            process.nextTick(() => {
                console.log('nextTick')
            })
        })
    })
})

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

Promise.resolve().then(function() {
    console.log('promise3')
})


//  nextTick
//  nextTick
//  nextTick
//  nextTick
//  promise0
//  promise3
//  timer2
//  promise2
//  timer1
//  promise1

Node的事件循环与浏览器差异

  • 浏览器的Event loop是在HTML5中定义的规范,而node中则由libuv库实现。
  • 浏览器环境中,微任务的任务队列是在每一个宏任务执行完成之后执行。node中,微任务会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行微任务队列的任务。

我们看一个栗子,来说明两者区别:

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

1、浏览器端运行情况

输出:

js 复制代码
// timer1
// promise1
// timer2
// promise2

浏览器端处理过程

2、Node端运行情况

node端运行需要分为两种情况:

  • 如果node11版本及之后,一旦执行一个阶段里的宏任务(setTimeout,setInterval,setImmediate)就会立刻执行微任务队列,跟浏览器端运行一致。最后结果:
js 复制代码
// timer1
// promise1
// timer2
// promise2
  • 如果是node10及之前版本,要看第一个定时器执行完,第二定时器是否在完成队列中。

如果第二个计时器未在完成队列中,结果为:

js 复制代码
// timer1
// promise1
// timer2
// promise2

如果第二个计时器未在完成队列中,结果为:

js 复制代码
  // timer1
  // timer2
  // promise1
  // promise2

我们来分析一下第二个计时器不在任务队列中的情况:

1、全局脚本main执行,将2个timer依次放入timer队列,main执行完后,调用栈为空闲,任务队列开始执行。

2、首先会进入timers阶段,执行timer1的回调函数,打印timer1,并将promsie1.then回调放入微任务队列。 同样的步骤执行timer2,打印timer2。

3、至此,timer阶段执行结束,EventLoop进入下一阶段之前,执行微任务队列的所有任务,依次打印promise1,promise2。

node端处理过程:

五、一道输出值面试题

看个栗子

浏览器端和node端执行输出:

js 复制代码
console.log('1');

setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
})
process.nextTick(function() {
    console.log('6');
})
new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8')
})

setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})

输出:1,7,6,8,2,4,3,5,9,11,10,12

一共三轮事件循环:

第一轮

宏任务:输出1;new Promise同步任务,输出7。

微任务队列中:process.nextTick属于微任务,输出6;然后输出8。

第一轮结束,输出1,7,6,8

第二轮

宏任务:输出2;new Promise同步任务,输出4。

微任务队列中:process.nextTick属于微任务,输出3;然后输出5。

第三轮

宏任务:输出9;new Promise同步任务,输出11。

微任务队列中:process.nextTick属于微任务,输出10;然后输出12。

Reference

1、nodejs.org/zh-cn/docs/...

2、developer.ibm.com/zh/language...

3、lynnelv.github.io/js-event-lo...

最后

感谢关注「松宝写代码」,欢迎三连(点赞,收藏,分享)

相关推荐
丁总学Java13 分钟前
微信小程序-npm支持-如何使用npm包
前端·微信小程序·npm·node.js
It'sMyGo23 分钟前
Javascript数组研究09_Array.prototype[Symbol.unscopables]
开发语言·javascript·原型模式
懒羊羊大王呀24 分钟前
CSS——属性值计算
前端·css
睡觉然后上课34 分钟前
c基础面试题
c语言·开发语言·c++·面试
李是啥也不会39 分钟前
数组的概念
javascript
无咎.lsy1 小时前
vue之vuex的使用及举例
前端·javascript·vue.js
fishmemory7sec1 小时前
Electron 主进程与渲染进程、预加载preload.js
前端·javascript·electron
fishmemory7sec1 小时前
Electron 使⽤ electron-builder 打包应用
前端·javascript·electron
豆豆2 小时前
为什么用PageAdmin CMS建设网站?
服务器·开发语言·前端·php·软件构建
JUNAI_Strive_ving2 小时前
番茄小说逆向爬取
javascript·python