面试后再谈 event loop 时间循环

发布于 2022-10-28 20:59:09 字数 3723 浏览 128 评论 0

先看 microtasks

这是一道常见的无聊的面试题:

setTimeout(() => {
  console.log(1);
}, 0);

new Promise((resolve, reject) => {
  console.log(2);
  for (let i = 0; i < 10000; i++) {
    i === 9999 && resolve();
  }
  console.log(4);
}).then(() => {
  console.log(5);
});

console.log(6);

相信仔细看过之前的 JavaScript 中的 tasks 与 microtasks 可以很快给出答案,在 Chrome 60.0 浏览器中的输出顺序为:2、4、6、5、1。

这道题主要是两个点:

  • Promise 属于 microtask ,而 setTimeout 回调属于 task ,对于 JavaScript 引擎而言, event loop 的优先级是不同的,所以 Promise resolve会先于 setTimeout 的回调输出
  • Promise 的构造函数参数是同步调用的,故2和4会先于6输出

Node 是单线程吗?

我们通常理解的是, JavaScript 引擎是单线程的,那问题就是,没有多线程,如何处理非同步I/O、网络请求逻辑?

其实我们知道,在 Node.js 中,我们使用的是异步I/O,通过让部分线程进行阻塞I/O或者非阻塞I/O加轮询技术来完成数据获取,让一个线程做计算处理,通过线程之间的通信将I/O得到的数据进行传递,这种异步I/O的方式就是线程池。

所以,对于 Node.js 而言,这些非同步的逻辑均是由线程池实现的,因此,说 Node 是单线程这个说法是不准确的,仅对于你书写的 JavaScript 代码而言,可以说 Node 是单线程。

事件循环

Node.js 进程启动时, Node.js 会创建一个类似于 while(true) 的循环,每执行一次循环体的过程我们称之为一次 Tick 。每次 Tick 的过程就是查看是否有事件待处理,如果有,就取出事件相关回调函数。如果存在关联的回调函数,就执行它们。然后进入下一次循环,如果不再有事件处理,就退出进程。

在每次 Tick 的过程中,如何判断事件需要处理呢?这里引入的概念是观察者。每个事件循环都有一个或者多个观察者,而判断是否有事件要处理的过程就是向这些观察者询问是否有要处理的事件。

Node 中,事件主要来源于网络请求、文件I/O等,这些事件对应的观察者有文件I/O观察者、网络I/O观察者等,观察者对事件进行分类。

事件循环是一个典型的生产者/消费者模型。异步I/O、网络请求等则是事件的生产者,源源不断为Node提供不同类型的事件,这些事件被传递到对应的观察者那里,事件循环则从观察者那里取出事件并处理。

这时,在 JavaScript 线程层面的异步调用第一阶段就结束了, JavaScript 线程可以继续执行当前任务的后续操作。当前的I/O操作在线程池中等待执行,不管它是否阻塞I/O,都不会影响到 JavaScript 线程的后续执行。一旦线程池中的任务执行完毕,便会通知到 JavaScript 线程执行相关回调。

process.nextTick() 方法与 setImmediate() 方法

JavaScript 自身所有的 setTimeoutsetInterval 两个非I/O的异步API之外, Node.js 自身还具有 process.nextTicksetImmediate 方法。

process.nextTick 不是 setTimeout(fn, 0) 的别名。它更加有效率。事件轮询随后的 Tick 调用,会在任何I/O事件(包括定时器)之前运行。

setImmediate 也是延时执行一个回调, callback 函数会按照它们被创建的顺序依次执行。 每次事件循环迭代都会处理整个回调队列。 如果一个立即定时器是被一个正在执行的回调排入队列的,则该定时器直到下一次事件循环迭代才会被触发。

如果 callback 不是一个函数,则抛出 TypeError

按照观察者来分, process.nextTick 属于 idle 观察者, setImmediate 属于 check 观察者。在每一轮事件循环中, idle 观察者先于 I/O 观察者, I/O 观察者先于 check 观察者。

看下面这个例子就明白了:

process.nextTick(() => {
  console.log('nextTick延时执行1');
});

process.nextTick(() => {
  console.log('nextTick延时执行2');
});

setImmediate(() => {
  console.log('setImmediate延时执行1');
  process.nextTick(() => {
    console.log('强势插入');
  });
});

setImmediate(() => {
  console.log('setImmediate延时执行2');
});

console.log('正常执行');

这段代码摘自 《深入浅出Node.js》 by 朴灵 ,但在我实际操作时,实际上由于现在已经是 Node 8.4.0 版本了,运行结果已经与原书不一致

读者可以自行下来运行一遍,便大概懂的几个观察者的优先级。

高性能 Node 服务器

一般说来服务器模型分为以下几种:

  • 同步式的服务,一次只能处理一个请求,处理时别的请求都处于等待状态。
  • 多线程的服务,如 Apache ,对每个请求启动一个线程来处理,但线程的创建和销毁以及切换需要消耗CPU资源,因而并不如单线程服务高效。
  • 基于事件的服务,如 Node.jsNginx ,无需进行线程上下文切换,消耗CPU资源较少。

然而,比起 Nginx ,虽然其自身具有反向代理和负载均衡机制,但其性能仍然不如 Node.js 服务器。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

文章
评论
28 人气
更多

推荐作者

櫻之舞

文章 0 评论 0

弥枳

文章 0 评论 0

m2429

文章 0 评论 0

野却迷人

文章 0 评论 0

我怀念的。

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文