Event Loop 是什么?
Event Loop 是什么?
本文写于 2020 年 12 月 6 日
广义上来说 Event Loop 并不是 JavaScript 独有的概念,他是一个计算机的通用概念。
狭义上来说,只有 Node.js 才有 Event Loop,浏览器并没有。
一个场景引发的困惑
为什么需要 Event Loop 呢?先看一个常见的场景,如果我们同时执行了三种不同的异步事件:
setTimeout(foo, 100);
fs.readFile('./README.md', bar);
server.on('close', doSth);
我们知道在计算机中,操作系统会帮我们新建一个「进程」使应用程序可以执行,但是一个进程在一个时刻只能执行一个任务,如果我们需要同时执行这三个任务,有三个方法:
- 新建一个进程;
- 新建一个线程;
- 排队执行。
如果不太了解,可以看我之前的一篇文章,「进程与线程」。
JavaScript 作为一门单线程的语言,肯定不能分出三条线程去执行这三条语句。那么假设计时器结束的一瞬间,三个回调函数同时触发了,Node 会怎么处理呢?
PS:Node.js 读取文件用的是 libuv,自己并不会去读写文件,所以单线程也可以异步的读写文件
我们可以推测出以下两点:
- Node.js 肯定会以某种顺序执行;
- 这种顺序应该是规定好的(优先级)。
Event Loop 的解释
回到 Event Loop 本身,我们拆开来看看。
什么叫做 Event?Event 就是事件,比如回调函数的触发就是一个事件。
什么叫做 Loop?Loop 就是循环,比如 for 循环 while 循环。
事件是有优先级的,所以处理时候是分先后的。Node.js 按照顺序去“询问”每个 Event,并且循环往复的去“询问”:
Event1 => Event2 => Event3 => Event1 => Event2 => ...
这就变成了 「poll(轮询)」。
操作系统触发事件,JavaScript 处理事件,Event Loop 就是事件处理顺序管理的「解决方案」。
维基是这么解释的:Event Loop 是一个程序结构,用于等待和发送消息和事件。
简单说,就是在程序中设置两个线程:
- 一个负责程序本身的运行,称为"主线程";
- 另一个负责主线程与其他进程的通信,被称为 「Event Loop 线程」,也有人叫他「消息线程」。
Node 中的 Event Loop
|-----------------------|
---->| timer |
| |-----------------------|
| |
| |
| |-----------------------|
| | close callbacks |
| |-----------------------|
| |
| |
| |-----------------------|
| | idle, prepare |
| |-----------------------|
| |
| | |------------------|
| |-----------------------| | incoming: |
| | poll |<----| connections, |
| |-----------------------| | data, etc. |
| | |------------------|
| |
| |-----------------------|
| | check |
| |-----------------------|
| |
| |
| |-----------------------|
-----| close callbacks |
|-----------------------|
这是 Node.js 官方文档上的示意图,我们来分析一下。
- 首先第一步,Node 会先去寻找是否存在计时器;
- 然后在看有没有其他的 I/O 相关的回调函数;
- idle 和 prepare 阶段根据字面意思推断,应该是清理一下,休息一下;
- 下一步进入轮询的阶段,检查系统事件;
- check 阶段检查 setImmediate 回调;
- close callbacks 就是处理类似 socket 关闭的事件。
这里可以看到 check 阶段检查的是 setImmediate 回调,有的面试题目可能会问 setImmediate(fn)
和 setTimeout(fn, 0)
谁先执行,这里我们就能回答了:不确定,因为 Event Loop 是个圈。
另外,setImmediate 不是一个标准特性,MDN 不建议我们在生产环境中使用它,浏览器只有新版的 IE 支持。
在 Node 的 Event Loop 中,最常用的就是:
- timer 检查;
- poll 轮询;
- check 检查。
我们先当作其他阶段都不存在了,只把这三个阶段作为循环。
因为 timer 阶段处理定时器函数回调,check 阶段处理 setImmediate 回调,所以如果这两个执行栈没有任务的话,我们就不会走这两个阶段了。
Node.js 会很智能的在其他阶段空闲时只停留在该阶段。
并且大部分时间,Node.js 都停留在 poll 阶段,文件请求、网络请求的事件处理也多半在这个阶段进行。
官方例子:
const fs = require('fs');
// 假设花费 95ms 读取文件
function someAsyncOperation() {
fs.readFile('/path/to/file', callback);
}
const timeoutScheduled = Date.now();
setTimeout(() => {
const delay = Date.now() - timeoutScheduled;
console.log(`${delay}ms 后执行了 setTimeout 的回调函数`);
}, 100);
// 95ms 的异步操作
someAsyncOperation(() => {
const startCallback = Date.now();
// 10ms 的同步操作
while (Date.now() - startCallback < 10) {
// nothing to do
}
});
这里我们花 95ms 读完文件后又做了 10ms 的同步操作。但问题是在 95ms 结束后 5ms 就需要执行定时器了呀,这里硬生生被 while
的同步操作卡住了 5ms,Node.js 在这个过程中是如何处理的呢?
- 当我们的 Event Loop 进入 poll 阶段,发现 poll 的队列是空的,因为文件没有读完。
- 于是 Event Loop 检查了一下最近的计时器,发现还需要 100ms,于是决定这段时间就停留在 poll 阶段。
- 在 poll 阶段停留了 95ms 之后,
readFile
操作完成,耗时 10ms 的同步操作被系统放入 poll 队列,执行完毕之后 poll 队列为空。 - Event Loop 紧接着又去看了一眼计时器,发现已经超时了 5ms 了。于是由经 check 阶段,close callbacks 阶段,Event Loop 又回到了 timer 阶段执行了超时计时器的回调函数。
这里如果我们一直占着 poll 阶段做同步任务会怎么样呢?
Node.js 做了限制,会有最长占用时长的,根据操作系统而定。
Event Loop 例题
我们来做几道例题吧:
例题一
fs.readFile('xx.txt', () => {
console.log('fs');
setTimeout(() => {
console.log('setTimeout');
}, 0);
setImmediate(() => {
console.log('setImmediate');
});
});
他们的输出顺序该是如何?
因为 readFile 是读取文件,该操作既不是 timer,也不是 setImmediate,所以在 poll 阶段进行。
poll 之后是 check,check 之后才是 timer。所以先执行 setImmediate,后执行 setTimeout。
例题二
setImmediate(() => {
console.log('setImmediate');
setTimeout(() => {
console.log('setTimeout in setImmediate');
}, 0);
});
setTimeout(() => {
console.log('setTimeout');
setImmediate(() => {
console.log('setImmediate in setTimeout');
});
}, 0);
这题应该分情况讨论。
情况一:
如果是 timer 先执行,那我们必然会先执行外层的 setTimeout 的回调函数。输出完 'setTimeout'
之后,将 setImmediate 的回调放入 check 阶段——注意,这里 check 的执行栈中已经有了第一个 setImmediate 了。
执行结束后,进入 check 阶段,执行第一个 setImmediate,输出 'setImmediate'
,然后执行第二个 setImmediate,输出 'setImmediate in setTimeout'
。再将 setTimeout 放入 timer 队列。执行结束。
进入 timer 队列,输出 'setTimeout in setImmediate'
。
情况二:略
例题三
console.log(1);
setTimeout(() => {
console.log('setTimeout1');
Promise.resolve('promise').then((res) => {
console.log(res);
});
}, 0);
setTimeout(() => {
console.log('setTimeout2');
}, 0);
这里我们会发现一个问题,刚刚 Node.js 的官方图没有给出任何关于 Promise 的细节,甚至于在原文中搜索 promise 也是一无所获。
那是因为 Node.js 的 Promise 是以 nextTick 为基础实现的。
nextTick 触发的时机是在当前任务队列结束之后立即执行。这里应该先输出 'setTimeout1'
,再输出 'promise'
,最后输出 'setTimeout2'
。
浏览器的 Event Loop
浏览器只有宏队列和微队列,比 Node.js 简单很多。(注意,这都不是 JS 引擎提供的,而是 Node 和浏览器提供的)。
- 宏列队:用来保存待执行的 macrotask (宏任务),比如:timer / DOM 事件监听 / ajax 回调;
- 微列队:用来保存待执行的 microtask(微任务),比如:Promise 的回调(then 方法) / MutationObserver 的回调。
在 Promise/A+的规范中,Promise 的实现可以是微任务,也可以是宏任务。但是普遍的共识表示(至少 Chrome 是这么做的),Promise 应该是属于微任务的。
当前执行栈执行完毕时,会立刻先处理所有「微任务」队列中的事件,然后再去「宏任务」队列中取出一个事件。也就是说同一次 Event Loop 中,微任务永远在宏任务之前执行。
说到这里其实很明白了,Node.js 中的 Event Loop 有很多阶段,不停的循环;而浏览器中只有两个阶段,分别查看宏任务和微任务队列。
看个例子:
setTimeout(() => console.log(1), 0);
new Promise((resolve) => {
console.log(2);
resolve('promise');
}).then((res) => {
console.log(res);
});
console.log(3);
- 当我们执行 setTimeout 的时候,将回调函数存入宏队列;
- 执行
new Promise
时,执行函数中的代码,将 then 任务存入微队列; - 执行
console.log(3)
,当前执行栈执行完毕; - 执行微队列;
- 执行宏队列。
做道题总结一下
OK,我们再看一道题目:
const foo = async () => {
console.log('foo');
await bar();
console.log('bar end');
};
const bar = async () => {
console.log('bar');
};
foo();
new Promise((resolve) => {
console.log('promise');
resolve('promise down');
}).then((res) => {
console.log(res);
});
首先第一步:因为 async
await
是 Promise 的语法糖,所以需要将思维转化成 Promise,不可以真的把这段代码当成同步代码。
- 所以先输出
'foo'
是没问题的,这相当于new Promise(() => { console.log('foo') })
一样; - 然后输出
'bar'
也是没问题的,相当于在第一次new
的Promise
中又new
了一次; - 接着输出
'bar end'
的语句应该被丢进微队列了; - 执行下方的
Promise
,输出'promise'
,将then
丢进微队列; - 执行微队列:先输出
'bar end'
,后输出'promise down'
。
(完)
Event Loop 是什么?的更多相关文章
- Atitit 解决Unhandled event loop exception错误的办法
Atitit 解决Unhandled event loop exception错误的办法 查看workspace/.metadata/.log org.eclipse.swt.SWTError: No ...
- javascript运行模式:并发模型 与Event Loop
看了阮一峰老师的JavaScript 运行机制详解:再谈Event Loop和[朴灵评注]的文章,查阅网上相关资料,把自己对javascript运行模式和EVENT loop的理解整理下,不一定对,日 ...
- [译]Node.js - Event Loop
介绍 在读这篇博客之前,我强列建议先阅读我的前两篇文章: Getting Started With Node.js Node.js - Modules 在这篇文章中,我们将学习 Node.js 中的事 ...
- Eclipse经常报Unhandled event loop exception的原因
在公司的电脑上,Eclipse经常报Unhandled event loop exception 错误,非常频繁,通过搜索发现是因为电脑上安装了百度杀毒导致的.... 无语 另外 teamviewer ...
- Node.js 事件循环(Event Loop)介绍
Node.js 事件循环(Event Loop)介绍 JavaScript是一种单线程运行但又绝不会阻塞的语言,其实现非阻塞的关键是“事件循环”和“回调机制”.Node.js在JavaScript的基 ...
- JavaScript 运行机制详解:再谈Event Loop
原文地址:http://www.ruanyifeng.com/blog/2014/10/event-loop.html 一年前,我写了一篇<什么是 Event Loop?>,谈了我对Eve ...
- PYTHON ASYNCIO: FUTURE, TASK AND THE EVENT LOOP
from :http://masnun.com/2015/11/20/python-asyncio-future-task-and-the-event-loop.html Event Loop On ...
- 数据密集型 和 cpu密集型 event loop
Node.js在官网上是这样定义的:“一个搭建在Chrome JavaScript运行时上的平台,用于构建高速.可伸缩的网络程序.Node.js采用的事件驱动.非阻塞I/O模型使它既轻量又高效,是构建 ...
- 【Node.js】Event Loop执行顺序详解
本文基于node 0.10.22版本 关于EventLoop是什么,请看阮老师写的什么是EventLoop 本文讲述的是EventLoop中的执行顺序(着重讲setImmediate, setTime ...
- [Javascript] Task queue & Event loop.
Javascript with Chorme v8 engine works like this : For Chorme engine, v8, it has call stack. And all ...
随机推荐
- 学习GlusterFS(一)
一.概述 1.GlusterFS是集群式NAS存储系统,分布式文件系统(POSIX兼容),Tcp/Ip方式互联的一个并行的网络文件系统,通过原生 GlusterFS 协议访问数据,也可以通过 NFS/ ...
- Vue报错之"[Vue warn]: Invalid prop: type check failed for prop "jingzinum". Expected Number with value NaN, got String with value "fuNum"."
一.报错截图 [Vue warn]: Invalid prop: type check failed for prop "jingzinum". Expected Number w ...
- 搞半天,全国34个省份包含湾湾\香港\澳门的高德poi兴趣点23类数据终于爬完事了
1.技术架构: python+阿里云数据库mongodb5.0+高德地图rest api 2.成本: 阿里云数据库mongodb5.0一个月话费1k多 2.遇到的问题 1)两个阿里云账号下 mongo ...
- 复习——高级语法对象原型,es5新增语法
今天的开始进入了js的高级语法 我马上也要复习完了,之前学到闭包递归,就回去复习去了,复都复习这么久而且,复习的过程真的比学知识的过程难熬的多,只不过终于要复习完了,再来点es6的新语法马上就要步入v ...
- SQL之总结(四)---null问题的处理
概述:如果表中的某个列是可选的,那么我们可以在不向该列添加值的情况下插入新记录或更新已有的记录.这意味着该字段将以 NULL 值保存. NULL 值的处理方式与其他值不同. NULL 用作未知的或不适 ...
- Codepen 每日精选(2018-3-31)
按下右侧的"点击预览"按钮可以在当前页面预览,点击链接可以打开原始页面. 制作像素画的画板https://codepen.io/abeatrize/... 纯 css 画的晚上的风 ...
- spark配置双master时一直处于standby的情况
一.情况描述 按照如下配置,使用zookeeper监听 SPARK_DAEMON_JAVA_OPTS="-Dspark.deploy.recoveryMode=ZOOKEEPER -Dspa ...
- Mybatis的简介+简单实现增删改查案例
@ 目录 总结内容 1. 基本概念 2. Mybatis的使用 需求 配置文件简介 总结 总结内容 1. 基本概念 Mybatis是一款优秀的持久层框架,它支持定制化SQL.存储过程以及高级映射.My ...
- audio小记
写H5活动页的需要音频,图标旋转停止保持当时的旋转角度,这样视觉体验效果好: 之前写法是点击pause()就直接停止动画,后来发现了animation有个比较好的属性animation-play-st ...
- 新版vue作用域插槽的使用
2.6开始,作用域插槽的使用有了不同的地方: 作用域插槽的个人理解就是让子组件的数据可以在父组件中使用: 也是一个数据传递的方式了: 不多说,上代码 子组件定义一个插槽,并且定义一个需要传递到父组件 ...