资讯专栏INFORMATION COLUMN

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

KevinYan / 2363人阅读

摘要:二浏览器端在讲解事件循环之前先谈谈中同步代码异步代码的执行流程。三端我自己认为的事件循环和浏览器端还是有点区别的,它的事件循环依靠引擎。四总结本篇主要介绍了浏览器和对于事件循环机制实现,由于能力水平有限,其中可能有误之处欢迎指出。

一、前言

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

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

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,下面分析步骤

调用c(),c入栈,此时栈中内容为:GContext->c-contextC

接着调用b(),b入栈,此时栈中内容为:GContext->c->contextC->b->contextB

接着调用a(),a入栈,此时栈中内容为:GContext->c->contextC->b->contextB-c->contextC

a执行完,a出栈,此时栈中内容为:GContext->c->contextC->b->contextB

b执行完,b出栈,此时栈中内容为:GContext->c->contextC

c执行完,b出栈,此时栈中内容为:GContext

全部执行完,释放资源

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

2.2、js异步代码执行过程

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

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

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引擎。

该图来自官网,这里展示了在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对于事件循环机制实现,由于能力水平有限,其中可能有误之处欢迎指出。

欢迎关注公众号:

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/109612.html

相关文章

  • JS与Node.js中的事件循环

    摘要:的单线程,与它的用途有关。特点的显著特点异步机制事件驱动。队列的读取轮询线程,事件的消费者,的主角。它将不同的任务分配给不同的线程,形成一个事件循环,以异步的方式将任务的执行结果返回给引擎。 这两天跟同事同事讨论遇到的一个问题,js中的event loop,引出了chrome与node中运行具有setTimeout和Promise的程序时候执行结果不一样的问题,从而引出了Nodejs的...

    abson 评论0 收藏0
  • 览器中的事件循环机制

    摘要:单线程的话,如果我们做一些的操作比如说这是一个耗时的操所那么在这将近一秒内,线程就会被阻塞,无法继续执行下面的任务。事件循环的主要机制就是任务队列机制一个事件循环有一个或者多个任务队列。 浏览器中的事件循环机制 网上一搜事件循环, 很多文章标题的前面会加上 JavaScript, 但是我觉得事件循环机制跟 JavaScript 没什么关系, JavaScript 只是一门解释型语言, ...

    zzbo 评论0 收藏0
  • 初窥JavaScript事件机制的实现(一)—— Node.js事件驱动实现概览

    摘要:如果当前没有事件也没有定时器事件,则返回。相关资料关于的架构及设计思路的事件讨论了使用线程池异步运行代码。下一篇初窥事件机制的实现二中定时器的实现 在浏览器中,事件作为一个极为重要的机制,给予JavaScript响应用户操作与DOM变化的能力;在Node.js中,事件驱动模型则是其高并发能力的基础。 学习JavaScript也需要了解它的运行平台,为了更好的理解JavaScript的事...

    lavor 评论0 收藏0
  • Node中的事件循环

    摘要:的事件循环一个线程有唯一的一个事件循环。索引就是指否还有需要执行的事件,是否还有请求,关闭事件循环的请求等等。先来看一下定义的定义是在事件循环的下一个阶段之前执行对应的回调。虽然是这样定义的,但是它并不是为了在事件循环的每个阶段去执行的。 Node中的事件循环 如果对前端浏览器的时间循环不太清楚,请看这篇文章。那么node中的事件循环是什么样子呢?其实官方文档有很清楚的解释,本文先从n...

    lwx12525 评论0 收藏0
  • 览器Node不同的事件循环(Event Loop)

    摘要:浏览器中与中事件循环与执行机制不同,不可混为一谈。浏览器环境执行为单线程不考虑,所有代码皆在执行线程调用栈完成执行。参考文章强烈推荐不要混淆和浏览器中的强烈推荐中的模块强烈推荐理解事件循环一浅析定时器详解 注意 在 node 11 版本中,node 下 Event Loop 已经与浏览器趋于相同。在 node 11 版本中,node 下 Event Loop 已经与浏览器趋于相同。在 ...

    haitiancoder 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<