浏览器和Node中的事件循环机制

0

一、前言

前几天听公司一个公司三年的前端说“今天又学到了一个知识点-微任务、宏任务”,我问他这是什么东西,由于在吃饭他浅浅的说了下,当时没太理解就私下学习整理一番,由于谈微任务、宏任务必谈到事件循环,于是就有了这篇博客。

在谈到事件循环机制之前我们需要知道一些基础知识就是:

  • js是单线程的
  • js一开始是作为脚本语言运行在客户端

其实js是单线程在它作为脚本语言操作dom的时候就决定了。那么此时就有一个性能问题,那么js在浏览器端是如何处理这个问题的呢?同时,js在后台Node中又是如何解决的呢?这就是本篇需要介绍的事件循环机制,这里我将分别以浏览器和Node两个方面来分析。

二、浏览器端

在讲解事件循环之前先谈谈js中同步代码、异步代码的执行流程。

2.1、js同步代码执行过程

js引擎在执行通过代码的过程中,会安装顺序依次存储到一个地方去,这个地方就叫做执行栈,当我们调用一个方法的时候,js会生成一个和这个方法相对应的上下文(context)。这个执行环境中存在着这个方法的私有作用域,上层作用域的指向,方法的参数,这个作用域中定义的变量以及这个作用域的this对象。

function a() {
    console.log("method a execute...");
}
function b() {
    a();
}
function c() {
    b();
}
c();

以上面例子分析:js在执行的时候会有一个全局上下文,我们这里就称为GContext,下面分析步骤

  1. 调用c(),c入栈,此时栈中内容为:GContext->c-contextC
  2. 接着调用b(),b入栈,此时栈中内容为:GContext->c->contextC->b->contextB
  3. 接着调用a(),a入栈,此时栈中内容为:GContext->c->contextC->b->contextB-c->contextC
  4. a执行完,a出栈,此时栈中内容为:GContext->c->contextC->b->contextB
  5. b执行完,b出栈,此时栈中内容为:GContext->c->contextC
  6. c执行完,b出栈,此时栈中内容为:GContext
  7. 全部执行完,释放资源

ok,上面是同步代码的执行,上面会涉及到两个核心概念:执行整个代码的线程我们称之为主线程,存放方法执行的地方我们称之为执行栈.

2.2、js异步代码执行过程

上面说完了同步过程,那这里来谈谈异步的过程。js引擎在遇到一个异步事件,不会一直等待返回结果而是将它挂起。当异步任务执行完之后会将结果加入到和执行栈中不同的任务队列当中,注意的是:此时放入队列不会立即执行其回调,而是当主线程执行完执行栈中所有的任务之后再去队列中查找是否有任务,如果有则取出排在第一位的事件然后将回调放入执行栈并执行其代码。如此反复就构成了事件循环。
image.png

这里同样有一个核心概念:任务队列

2.3、微任务、宏任务

上面提到js执行异步方法的时候会将其返回结果放到队列中,这是比较笼统的,具体来说,js会根据任务的类型将其放入不同的队列,任务类型有两种:微任务、宏任务。那么其对应的哪些是微任务、哪些是宏任务呢?

  • 微任务:Promise、process.nextTick()、整体代码script、Object.observer、MutationObserver
  • 宏任务:setTimeout()、setInterval()

浏览器在执行的时候,先从宏任务队列中取出一个宏任务执行宏,然后在执行该宏任务下的所有的微任务,这是一个循环;然后再取出并执行下一个宏任务,再执行所有的微任务,这是第二个循环,以此类推.

注意:整个javascript代码是第一个宏任务
const process = require('process')
setTimeout(function () {// 分发宏任务到EventQueue
    console.log("1");
}, 0);
setTimeout(() => {
    console.log("11");
}, 0);
setTimeout(() => {
    console.log("111");
}, 0);
new Promise(function (resolve) {
    console.log('2');
    resolve();
}).then(function () {// 发送微任务
    console.log('3');
});
// 输出
2
3
1
11
111

2.4、小结

在浏览器端,在我们执行一片script的时候,当遇到同步代码,依次进入执行栈,遇到异步代码,将其挂起,继续执行其它方法,当异步方法执行完之后根据任务类型进入到任务队列,在执行栈执行完,主线程空闲下来了之后会到任务队列中取任务回调并执行。

三、Node端

我自己认为Node的事件循环和浏览器端还是有点区别的,它的事件循环依靠libuv引擎。

image.png

该图来自官网,这里展示了在node的事件循环的6个阶段。

  • timers:该阶段执行定时器的回调,如setTimeout() 和 setInterval()。
  • I/O callbacks:该阶段执行除了close事件,定时器和setImmediate()的回调外的所有回调
  • idle, prepare:内部使用
  • poll:等待新的I/O事件,node在一些特殊情况下会阻塞在这里
  • check: setImmediate()的回调会在这个阶段执行
  • close callbacks: 例如socket.on('close', ...)这种close事件的回调

对于我们来说我们更关注 timer、poll、check这三个阶段即可。

poll 阶段有两个主要的功能:

  • 处理poll队列(poll quenue)的事件(callback);
  • 执行timers的callback,当到达timers指定的时间时;

poll 阶段的逻辑

  • 如果event loop进入了 poll阶段,且代码未设定timer,将会发生下面情况:

    • a、如果poll queue不为空,event loop将同步的执行queue里的callback,直至queue为空,或执行的callback到达系统上限;
    • b、如果poll queue为空,将会发生下面情况:

      * 如果代码已经被setImmediate()设定了callback, event loop将结束poll阶段进入check阶段,并执行check阶段的queue (check阶段的queue是 setImmediate设定的)
      * 如果代码没有设定setImmediate(callback),event loop将阻塞在该阶段等待callbacks加入poll queue;
      
  • 如果event loop进入了 poll阶段,且代码设定了timer:

    • 如果poll queue进入空状态时(即poll 阶段为空闲状态),event loop将检查timers,
    • 如果有1个或多个timers时间时间已经到达,event loop将按循环顺序进入 timers 阶段,并执行timer queue

3.1、setTimeout、setImmediate

这两个函数的功能还是类似的,不同的是他们处于EventLoop的不同阶段:timer、check。

setImmediate(()=>console.log("setInterval"));
setTimeout(() => {console.log("setTimeout")},0);

上面两行代码会输出顺序是什么呢?其实两种可能都有.
1.当setTimeout的0ms并不能做到绝对0ms,如果已经过了timer阶段,那么此时setTimeout就会在下一次循环中执行,也就是说先setInterval、再setTimeout。
2.第二种可能就是正常流程了,先timer、再check

如果上面的代码再一个IO操作作呢?如:

require('fs').readFile(__filename,()=>{
    setImmediate(()=>console.log("setInterval"));
    setTimeout(() => {console.log("setTimeout")});
})

此时只可能出现一种情况,先setInterval、再setTimeout,因为在io中已经执行过了timer(readFile时处于IO callback)。
下面一起来看如下代码:

setTimeout(() => {
    console.log("timer1")
    Promise.resolve().then(() => console.log("promise1"));
    process.nextTick(() => console.log("nextTick1"))
}, 0);
setTimeout(() => {
    console.log("timer2")
    Promise.resolve().then(() => console.log("promise2"));
    process.nextTick(() => console.log("nextTick2"))
}, 0);

按照我的理解,它的输出应该是如下:先timer、然后切换阶段的时候执行微任务.

// 情况1
timer1
timer2
nextTick1
nextTick2
promise1
promise2

可是并不是,它的输出一直是:

// 情况2
timer1
nextTick1
promise1
timer2
nextTick2
promise2

后台晚上查资料因为Node11对EventLoop作了修改,为了和浏览器兼容。于是呼我切换到10.8.0,发现上面两种情况都有(情况1比例大于情况2)。这点暂时还未查明什么原因。

3.2、小结

node中的6个阶段每个阶段执行完都会伴随着执行微任务,同个MicroTask队列下process.tick()会优于Promise。

四 总结

本篇主要介绍了浏览器和Node对于事件循环机制实现,由于能力水平有限,其中可能有误之处欢迎指出。

欢迎关注公众号:
码农有道

你可能感兴趣的

载入中...