- Java锁的逻辑(结合对象头和ObjectMonitor)
- 还在用饼状图?来瞧瞧这些炫酷的百分比可视化新图形(附代码实现)⛵
- 自动注册实体类到EntityFrameworkCore上下文,并适配ABP及ABPVNext
- 基于Sklearn机器学习代码实战
本篇将对以下问题进行讨论:
有关 事件循环 前文已经略作介绍,这里进一步补充.
请在浏览器中运行这段代码:
console.log('1');
setTimeout(() => {
console.log('2');
Promise.resolve().then(() => console.log('3'));
Promise.resolve().then(() => console.log('4'));
}, 100);
setTimeout(() => {
console.log('5');
Promise.resolve().then(() => console.log('6'));
}, 150);
Promise.resolve().then(() => console.log('7'));
setTimeout(() => console.log('8'), 200);
console.log('9');
/*
结果:
1
9
7
2
3
4
5
6
8
*/
分析这段代码的事件循环的详细过程之前,有几点需要说一下:
一个宏任务
和 所有的微任务
,而且宏任务和微任务的处理顺序是固定的:每次执行完一个宏任务后,首先会立即处理所有的微任务,然后才会执行下一个宏任务。如果在执行微任务时又产生了新的微任务,那么这些新的微任务也会被添加到队列中,直到全部微任务都执行完成,才会执行宏任务。 轮询
方式来实现事件循环机制。在执行完当前的任务之后,如果宏任务队列为空,主线程会等待一段时间,这个时间间隔是由浏览器厂商自行决定的,然后再次查询宏任务队列是否有任务需要执行。 setTimeout(() => console.log('8'), 200)
,浏览器会创建一个定时器(200ms),并将回调函数和指定的时间保存在一个任务中。当指定的时间到达时,定时器才会将这个任务推入宏任务队列中等待处理 这段代码大概有 四次 事件循环,执行过程如下:
首先将 console.log('1') 加入执行栈中,输出 1,然后将其从执行栈中弹出。
第一个 setTimeout 函数被调用时,浏览器会创建一个定时器(100ms),并将回调函数和指定的时间保存在一个任务中。当指定的时间到达时,定时器会将这个任务推入宏任务队列中等待处理
第二个 setTimeout 与第一 setTimeout 类似,等待 150ms 后会被放入宏任务队列中
Promise.resolve().then(() => console.log('7')) 放入微任务队列
第三个 setTimeout 与第一 setTimeout 类似,等待 200ms 后会被放入宏任务队列中
执行 console.log('9')
取出微任务队列中的所有任务,输出 7
执行栈为空,主线程轮询查看宏任务队列(微任务队列刚才已经清空了),此时宏任务队列为空
100ms后,第一个setTimeout 宏任务推入宏任务队列中,取出这个宏任务放入执行栈中
输出 2
执行 `Promise.resolve().then(() => console.log('3'));`、`Promise.resolve().then(() => console.log('4'));`,放入微任务队列
这个宏任务执行完毕之后,主线程会转而执行当前微任务队列中的所有任务,输出 3 和 4
执行栈为空,主线程轮询宏任务队列发现其为空
150ms后,第二个setTimeout 宏任务推入宏任务队列中,取出这个宏任务放入执行栈中
输出 5
执行 `Promise.resolve().then(() => console.log('6'));` 放入微任务队列
这个宏任务执行完毕之后,主线程会转而执行当前微任务队列中的所有任务,输出 6
执行栈为空,主线程轮询宏任务队列发现其为空
200ms后,第三个setTimeout 宏任务推入宏任务队列中,取出这个宏任务放入执行栈中
输出 8
宏任务之间其实存在 优先级 。比如 click > requestAnimationFrame > setTimeout .
紧急
,从而使它们具有比其他任务更高的优先级。这确保了与用户直接交互相关的操作具有更快的响应时间。 至少
在指定的时间后才开始执行 请看示例:
function log(message) {
const now = new Date();
console.log(`[${now.getSeconds()}:${now.getMilliseconds()}] ${message}`);
}
setTimeout(() => {
log('setTimeout callback');
}, 0);
requestAnimationFrame(() => {
log('requestAnimationFrame callback');
});
document.addEventListener('click', () => {
log('click event');
});
// 手动触发 click 事件
const event = new Event('click');
document.dispatchEvent(event);
/*
[46:280] click event
[46:299] setTimeout callback
[5:646] requestAnimationFrame callback
*/
无论测试多少次,click 总是最先输出。但是 requestAnimationFrame 就不一定先 setTimeout 输出,因为 requestAnimationFrame 有自己的节奏,只要不影响平滑的动画效果,即使在 setTimeout 后面也可能.
Node.js 核心的特性是 事件驱动 (Event-driven)和 非阻塞 I/O (Non-blocking I/O)
事件驱动
- nodejs 中的异步操作基于事件,也就是说,当某个操作完成时,Node.js 会发出一个事件来通知你,然后你就可以通过注册事件的方式来执行回调函数。 非阻塞 I/O
- nodejs 执行一个 I/O 操作时,它不会像传统的同步阻塞 I/O 一样等待操作完成,而是会在操作的同时继续处理其他请求。这种方式可以避免 I/O 导致的阻塞,提高系统的吞吐量和响应能力。 Tip :两个特性有关系,但不是一个概念。比如可以说:基于事件驱动的 非阻塞 I/O 。
Node.js 中的事件驱动和非阻塞 I/O 是基于事件循环实现的.
在 node 中,事件循环是一个持续不断的循环过程,不断地从 事件队列 中取出事件并处理,直到事件队列为空。具体来说,当 Node.js 遇到一个需要异步处理的 I/O 操作时,它不会等待操作完成后再执行下一步操作,而是将该操作放到事件队列中,并继续执行下一步。当操作完成后,Node.js 会将相应的回调函数也放到事件队列中,等待事件循环来处理。这样一来,Node.js 就可以同时处理多个请求,而且不会因为某一个操作的阻塞而影响整个应用程序的性能.
除了 I/O 操作之外,事件循环还可以用于处理 定时器 、 HTTP 请求 、 数据库访问 等各种类型的事件 。
Tip : 事件队列不仅包含 宏任务队列 和 微任务队列 ,还有维护着几个其他的队列,这些队列通过事件循环机制来实现异步非阻塞。其他队列有:
在 Node.js 中, 高并发 指的是系统能够处理高并发请求的能力。不会因为一个请求的处理而阻塞其他请求的执行,系统能够同时处理众多请求。 高性能 通常指的是它在处理大量并发请求时表现出的优异性能.
事件循环是 Node.js 实现高并发和高性能的 核心机制 之一。通过将计算密集型任务和 I/O 任务分离并采用异步执行,Node.js 能够充分利用 CPU 和内存资源,从而实现高性能和高并发.
没有事件循环 ,Node.js 就无法实现异步 I/O 和非阻塞式编程模型。在传统的阻塞式 I/O 模型中,一个 I/O 操作会一直等待数据返回,导致应用程序被阻塞,无法进行其他操作。而通过事件循环机制,Node.js 实现了异步 I/O,当一个 I/O 操作被触发后,Node.js 将其放入事件循环队列中,然后立即执行下一个任务,不必等待当前的 I/O 操作结束。当 I/O 操作完成时,Node.js 会将相应的回调函数添加到事件队列中等待执行.
相同点:单个主线程、单个执行栈、有宏任务队列和微任务队列 。
不同点:
实现
不同。Node.js 是一款服务端运行时,而浏览器则用于页面和交互等,场景不同,所以实现方式不同。Node.js 中的事件循环机制是通过 libuv 库来实现,因为它具有跨平台性、高效性、多功能性(除了事件循环机制外,libuv 还提供了很多其他的系统功能和服务,能够满足 Node.js 在服务器端编程上的需要)等。 一次事件循环
不同。浏览器中的一次事件循环包括一个宏任务和相关所有微任务。在 node 中,一次事件循环包含6个阶段(下文会详细介绍) 虽然两者有不同,但它们有 相同的设计目标 :高效而可靠的方式处理 异步任务 (或者说:解决 JavaScript 异步编程问题).
一次事件循环包含以下 6 个阶段:
+--------------------------+
| |
| timers | 计时器阶段:处理 setTimeout() 和 setInterval() 定时器的回调函数。
| |
+--------------------------+
| |
| pending callbacks | 待定回调阶段:用于处理系统级别的错误信息,例如 TCP 错误或者 DNS 解析异常。
| |
+--------------------------+
| |
| idle, prepare | 仅在内部使用,可以忽略不计。
| |
+--------------------------+
| |
| poll | 轮询阶段:等待 I/O 事件(如网络请求或文件 I/O 等)的发生,然后执行对应的回调函数,并且会处理定时器相关的回调函数。
| | 如果没有任何 I/O 事件发生,此阶段可能会使事件循环阻塞。
+--------------------------+
| |
| check | 检查阶段:处理 setImmediate() 的回调函数。check 的回调优先级比 setTimeout 高,比微任务要低
| |
+--------------------------+
| |
| close callbacks | 关闭回调阶段:处理一些关闭的回调函数,比如 socket.on('close')。
| |
+--------------------------+
这 6 个阶段执行顺序:
timers
阶段,执行所有超时时间到达的定时器相关的回调函数。 pending callbacks
阶段。在这个阶段, Node.js 会执行一些系统级别的回调函数,这些回调函数一般都是由 Node.js 的内部模块触发的,而不是由 JavaScript 代码直接触发的。 poll
阶段,等待 I/O 事件的发生,处理相关的回调函数。如果在此阶段确定没有任何 I/O 事件需要处理,那么事件循环会等待一定的时间,以防止 CPU 空转,这个时间会由系统自动设置或者手动在代码中指定。如果有定时器在此阶段需要处理,那么事件循环会回到 timers 阶段继续执行相应的回调函数。 check
阶段,处理 setImmediate() 注册的回调函数。setImmediate() 的优先级比 timers 阶段要高。当事件循环进入 check 阶段时,如果发现事件队列中存在 setImmediate() 的回调函数,则会立即执行该回调函数而不是继续等待 timers 阶段的到来。 close callbacks
阶段,处理一些关闭的回调函数。 事件循环的每个阶段都有对应的 宏任务队列 和 微任务队列 。当一个阶段中的所有宏任务都执行完之后,事件循环会进入下一个阶段。在该阶段结束时,如果存在微任务,事件循环将会在开始下一个阶段之前执行所有的微任务。这样一来,无论在何时添加微任务,都能确保先执行所有的微任务,避免了某些任务的并发问题。如果我们在某个阶段中添加了多个微任务,那么它们会在该阶段结束时依次执行,直到所有微任务都被处理完成,才会进入下一个阶段的宏任务队列.
一次事件循环周期 以清空6个阶段的宏任务队列和微任务队列来结束.
一次事件循环周期内, 每个阶段是否可以执行多次 。例如此时在 poll 阶段,这时 timers 阶段任务队列中有了回调函数,由于 timers 的优先级高于 poll,所以又回到 timers 阶段,执行完该阶段的宏任务和微任务后,在回到 poll 阶段.
总之,这 6 个阶段构成了 Node.js 的事件循环机制,确保了所有被注册的回调函数都能得到及时、准确的执行 。
Tip :当调用 setTimeout 方法时,如果超时时间还没到,则生成的定时器宏任务也 不会立刻 放入宏任务队列中,而是会被放入计时器队列中。计时器队列和延迟队列类似,都是由定时器宏任务组成的小根堆结构,每个定时器宏任务也对应着其到期时间以及对应的回调函数。当超时时间到达后,Node.js 会将该定时器宏任务从计时器队列中取出并放入宏任务队列中,等待事件循环去执行.
尽管事件循环的机制比较明确,但由于各种因素的影响,具体的执行顺序仍然 难以精确预测 。其顺序取决于当前事件队列中各个回调函数的执行情况、耗时以及系统各种资源的利用情况等多种因素。每次事件循环的顺序都不一定相同:
Tip : setTimeout 在node 中最小是 1ms ,在浏览器中是 4ms .
console.log("start");
setTimeout(() => {
console.log("first timeout callback");
}, 1);
setImmediate(() => {
console.log("immediate callback");
});
process.nextTick(() => {
console.log("next tick callback");
});
console.log("end");
运行10次node 输出如下:
start
end
next tick callback
first timeout callback
immediate callback
执行分析:
start
、 end
next tick callback
现在的难点是 setImmediate 和 setTimeout 的回调哪个先执行! 。
注 :在某些特殊情况下,timers 阶段和 check 阶段的任务可能会交错执行。这通常发生在以下两种情况下:
根据结果,我们 推测 :setImmediate 和 setTimeout 都进入了下一个循环周期,先执行 timers 阶段,在执行 check 阶段的回调.
Tip : 尽管 setImmediate 被称为 "immediate",但它并不保证会立刻执行。在 Node.js 的事件循环中,setImmediate() 的回调函数会被加入到 check 阶段的任务队列中,等到轮到 check 阶段时才会执行.
Node.js 不适合 CPU 密集型场景 。比如大量数学计算,可能会阻塞 Node.js 主线程.
比如一个 1 到 10亿求和 的请求:
const http = require('http');
http.createServer((req, res) => {
console.log('start');
let sum = 0;
for (let i = 1; i <= 1000000000; i++) {
sum += i;
}
console.log('end');
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end(sum.toString());
}).listen(3000);
console.log('server running at http://localhost:3000/');
通过curl 检测访问 http://localhost:3000/ 的时间,分别是 1.754s 、 1.072s 、 2.821s 。
Administrator@ MINGW64 /e/ (master)
$ time curl http://localhost:3000/
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 18 0 18 0 0 15 0 --:--:-- 0:00:01 --:--:-- 15500000000067109000
real 0m1.754s
user 0m0.000s
sys 0m0.078s
Administrator@ MINGW64 /e/ (master)
$ time curl http://localhost:3000/
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 18 0 18 0 0 20 0 --:--:-- --:--:-- --:--:-- 21500000000067109000
real 0m1.072s
user 0m0.015s
sys 0m0.093s
Administrator@ MINGW64 /e/ (master)
$ time curl http://localhost:3000/
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 18 0 18 0 0 6 0 --:--:-- 0:00:02 --:--:-- 6500000000067109000
real 0m2.821s
user 0m0.031s
sys 0m0.077s
接着用node 内置的 cluster 模块将计算工作分配到4个子进程中,访问速度大幅度提升.
const http = require('http');
const cluster = require('cluster');
if (cluster.isMaster) {
// 计算工作分配到4个子进程中
const numCPUs = require('os').cpus().length;
const range = 1000000000;
const rangePerCore = Math.ceil(range / numCPUs);
let endIndex = 0;
let sum = 0;
for (let i = 0; i < numCPUs; i++) {
const worker = cluster.fork();
worker.on('message', function({ endIndex, result }) {
sum += result;
if (endIndex === range) {
console.log(sum);
// 启动 Web 服务器,在主进程中处理请求
http.createServer((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end(`The sum is ${sum}\n`);
}).listen(3000, () => {
console.log(`Server running at http://localhost:3000/`);
});
}
});
worker.send({ startIndex: endIndex + 1, endIndex: endIndex + rangePerCore });
endIndex += rangePerCore;
}
} else {
process.on('message', function({ startIndex, endIndex }) {
let sum = 0;
for (let i = startIndex; i <= endIndex; i++) {
sum += i;
}
process.send({ endIndex, result: sum });
});
}
访问时长分别是: 0.230s 、 0.216s 、 0.205s :
Administrator@ MINGW64 /e/ (master)
$ time curl http://localhost:3000/
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 30 100 30 0 0 2354 0 --:--:-- --:--:-- --:--:-- 4285The sum is 500000000098792260
real 0m0.230s
user 0m0.000s
sys 0m0.109s
Administrator@ MINGW64 /e/ (master)
$ time curl http://localhost:3000/
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 30 100 30 0 0 2212 0 --:--:-- --:--:-- --:--:-- 3750The sum is 500000000098792260
real 0m0.216s
user 0m0.000s
sys 0m0.078s
Administrator@ MINGW64 /e/ (master)
$ time curl http://localhost:3000/
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 30 100 30 0 0 2545 0 --:--:-- --:--:-- --:--:-- 6000The sum is 500000000098792260
real 0m0.205s
user 0m0.000s
sys 0m0.078s
假如一个请求得花费2秒( 1 到 10亿之和 ),使用 pm2 也不能减小请求时间.
pm2能做的是:比如一个 node 应用单核(1个cpu内核)可以支持一千个并发请求,现在并发四千个请求,由于超出能力,请求响应会变慢。现在通过 Pm2 在四核服务器中启动4个node应用,之前还存在负载均衡,这样就可以支持四千个并发请求.
Tip :pm2的介绍请看 这里 。
Node.js 是单线程的,这意味着所有事件循环(Event Loop)和 I/O 操作都在 一个主线程 中运行。所以说,Node.js 中只存在一个事件循环和一个执行上下文栈.
不过,Node.js 的实现并不简单粗暴。它通过使用非阻塞 I/O、异步编程以及事件驱动机制,让单线程可以支持高并发处理大量的 I/O 操作。Node.js 底层采用的是 libuv 库来实现异步 I/O 模型,该库在底层会使用 libev 和 libeio 等多种事件驱动框架来实现对底层 I/O 系统调用的封装,从而让单线程可以同时处理多个 I/O 任务,避免了线程切换的开销,提高了应用程序的性能.
此外,在 Node.js 版本 10.5.0 之后,Node.js 引入了 worker_threads 模块,支持通过创建子线程的方式来实现多线程。worker_threads 模块提供了一套 API,使得开发者可以方便地创建和管理多个子线程,并利用多线程来加速处理计算密集型任务等场景.
总之,Node.js 是单线程的,但同时也通过采用异步 I/O 模型、事件驱动机制和多线程等技术手段,来支持高并发、高性能的应用程序开发.
最后此篇关于前端学习node快速入门系列——事件循环的文章就讲到这里了,如果你想了解更多关于前端学习node快速入门系列——事件循环的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
我是 PHP 新手。我一直在脚本中使用 for 循环、while 循环、foreach 循环。我想知道 哪个性能更好? 选择循环的标准是什么? 当我们在另一个循环中循环时应该使用哪个? 我一直想知道要
我在高中的编程课上,我的作业是制作一个基本的小计和顶级计算器,但我在一家餐馆工作,所以制作一个只能让你在一种食物中读到。因此,我尝试让它能够接收多种食品并将它们添加到一个价格变量中。抱歉,如果某些代码
这是我正在学习的一本教科书。 var ingredients = ["eggs", "milk", "flour", "sugar", "baking soda", "baking powder",
我正在从字符串中提取数字并将其传递给函数。我想给它加 1,然后返回字符串,同时保留前导零。我可以使用 while 循环来完成此操作,但不能使用 for 循环。 for 循环只是跳过零。 var add
编辑:我已经在程序的输出中进行了编辑。 该程序要求估计给定值 mu。用户给出一个值 mu,同时还提供了四个不等于 1 的不同数字(称为 w、x、y、z)。然后,程序尝试使用 de Jaeger 公式找
我正在编写一个算法,该算法对一个整数数组从末尾到开头执行一个大循环,其中包含一个 if 条件。第一次条件为假时,循环可以终止。 因此,对于 for 循环,如果条件为假,它会继续迭代并进行简单的变量更改
现在我已经习惯了在内存非常有限的情况下进行编程,但我没有答案的一个问题是:哪个内存效率更高;- for(;;) 或 while() ?还是它们可以平等互换?如果有的话,还要对效率问题发表评论! 最佳答
这个问题已经有答案了: How do I compare strings in Java? (23 个回答) 已关闭 8 年前。 我正在尝试创建一个小程序,我可以在其中读取该程序的单词。如果单词有 6
这个问题在这里已经有了答案: python : list index out of range error while iteratively popping elements (12 个答案) 关
我正在尝试向用户请求 4 到 10 之间的整数。如果他们回答超出该范围,它将进入循环。当用户第一次正确输入数字时,它不会中断并继续执行 else 语句。如果用户在 else 语句中正确输入数字,它将正
我尝试创建一个带有嵌套 foreach 循环的列表。第一个循环是循环一些数字,第二个循环是循环日期。我想给一个日期写一个数字。所以还有另一个功能来检查它。但结果是数字多次写入日期。 Out 是这样的:
我想要做的事情是使用循环创建一个数组,然后在另一个类中调用该数组,这不会做,也可能永远不会做。解决这个问题最好的方法是什么?我已经寻找了所有解决方案,但它们无法编译。感谢您的帮助。 import ja
我尝试创建一个带有嵌套 foreach 循环的列表。第一个循环是循环一些数字,第二个循环是循环日期。我想给一个日期写一个数字。所以还有另一个功能来检查它。但结果是数字多次写入日期。 Out 是这样的:
我正在模拟一家快餐店三个多小时。这三个小时分为 18 个间隔,每个间隔 600 秒。每个间隔都会输出有关这 600 秒内发生的情况的统计信息。 我原来的结构是这样的: int i; for (i=0;
这个问题已经有答案了: IE8 for...in enumerator (3 个回答) How do I check if an object has a specific property in J
哪个对性能更好?这可能与其他编程语言不一致,所以如果它们不同,或者如果你能用你对特定语言的知识回答我的问题,请解释。 我将使用 c++ 作为示例,但我想知道它在 java、c 或任何其他主流语言中的工
这个问题不太可能帮助任何 future 的访问者;它只与一个小的地理区域、一个特定的时间点或一个非常狭窄的情况有关,这些情况并不普遍适用于互联网的全局受众。为了帮助使这个问题更广泛地适用,visit
我是 C 编程和编写代码的新手,以确定 M 测试用例的质因数分解。如果我一次只扫描一次,该功能本身就可以工作,但是当我尝试执行 M 次时却惨遭失败。 我不知道为什么 scanf() 循环有问题。 in
这个问题已经有答案了: JavaScript by reference vs. by value [duplicate] (4 个回答) 已关闭 3 年前。 我在使用 TSlint 时遇到问题,并且理
我尝试在下面的代码中添加 foreach 或 for 循环,以便为 Charts.js 创建多个数据集。这将允许我在此折线图上创建多条线。 我有一个 PHP 对象,我可以对其进行编码以稍后填充变量,但
我是一名优秀的程序员,十分优秀!