深入理解异步事件驱动编程(二)

介绍

介绍同深入理解异步事件驱动编程(一)

迟执行

有时需要推迟执行一个函数。传统的 JavaScript 使用定时器来实现这一目的,使用众所周知的setTimeoutsetInterval函数。Node 引入了另一种推迟执行的方式,主要是作为控制回调函数在 I/O 事件和定时器事件之间执行顺序的手段。

正如我们之前看到的,管理定时器是 Node 事件循环的主要工作之一。两种延迟事件源,使开发人员能够安排回调函数的执行在排队的 I/O 事件之前或之后,分别是process.nextTicksetImmediate。现在让我们来看看这些。

process.nextTick

作为原生 Node 进程模块的一种方法,process.nextTick类似于熟悉的setTimeout方法,它延迟执行其回调函数直到将来的某个时间点。然而,这种比较并不完全准确;所有请求的nextTick回调函数列表都被放在事件队列的头部,并在当前脚本的执行之后(JavaScript 代码在 V8 线程上同步执行)和 I/O 或定时器事件之前,按顺序处理。

在函数中使用nextTick的主要目的是,将结果事件的广播推迟到当前执行堆栈上的监听器在调用者有机会注册事件监听器之前,给当前执行的程序一个机会将回调绑定到EventEmitter.emit事件。

把这看作是一个模式,可以在任何想要创建自己的异步行为的地方使用。例如,想象一个查找系统,可以从缓存中获取,也可以从数据存储中获取新鲜数据。缓存很快,不需要回调,而数据 I/O 调用需要它们。

第二种情况中回调的需求支持对回调行为的模拟,在第一种情况中使用nextTick。这允许一致的 API,提高了实现的清晰度,而不会使开发人员负担起确定是否使用回调的责任。

以下代码似乎设置了一个简单的事务;当EventEmitter的一个实例发出开始事件时,将Started记录到控制台:

ini 复制代码
const events = require('events');
function getEmitter() {
  let emitter = new events.EventEmitter();
  emitter.emit('start');
  return emitter;
}

let myEmitter = getEmitter();

myEmitter.on("start", () => {
  console.log("Started");
});

然而,你可能期望的结果不会发生!在getEmitter中实例化的事件发射器在返回之前发出start,导致后续分配的监听器出现错误,它到达时已经晚了一步,错过了事件通知。

为了解决这种竞争条件,我们可以使用process.nextTick

ini 复制代码
const events = require('events');
function getEmitter() {
  let emitter = new events.EventEmitter();
  process.nextTick(() => {
    emitter.emit('start');
  });
  return emitter;
}

let myEmitter = getEmitter();
myEmitter.on('start', () => {
  console.log('Started');
});

这段代码在 Node 给我们start事件之前附加了on("start")处理程序,并且可以正常工作。

错误的代码可能会递归调用nextTick,导致代码无休止地运行。请注意,与在事件循环的单个轮次内对函数进行递归调用不同,这样做不会导致堆栈溢出。相反,它会使事件循环饥饿,使微处理器上的进程繁忙,并可能阻止程序发现 Node 已经完成的 I/O。

setImmediate

setImmediate在技术上是定时器类的成员,与setIntervalsetTimeout一起。但是,它与时间无关------没有毫秒数等待发送参数。

这个方法实际上更像是process.nextTick的一个同级,有一个非常重要的区别:通过nextTick排队的回调将在 I/O 和定时器事件 之前执行,而通过setImmediate排队的回调将在 I/O 事件 之后调用。

这两种方法的命名令人困惑:Node 实际上会在你传递给setImmediate的函数之前运行你传递给nextTick的函数。

这个方法确实反映了定时器的标准行为,它的调用将返回一个对象,可以传递给clearImmediate,取消你对以后运行函数的请求,就像clearTimeout取消使用setTimeout设置的定时器一样。

定时器

定时器用于安排将来的事件。当需要延迟执行某些代码块直到指定的毫秒数过去时,用于安排特定函数的周期性执行等等时,就会使用它们。

JavaScript 提供了两个异步定时器:setInterval()setTimeout()。假设读者完全了解如何设置(和取消)这些定时器,因此将不会花费太多时间讨论语法。我们将更多地关注定时和间隔的陷阱和不太为人知的细节。

关键要点是:在使用定时器时,不应该对定时器触发注册的回调函数之前实际过去的时间量或回调的顺序做任何假设。Node 定时器不是中断。定时器只是承诺尽可能接近指定的时间执行(但绝不会提前),与其他事件源一样,受事件循环调度的约束。

关于定时器你可能不知道的一件事是-我们都熟悉setTimeout的标准参数:回调函数和超时间隔。你知道传递给callback函数的还有许多其他参数吗?setTimeout(callback, time, [passArg1, passArg2...])

setTimeout

超时可以用来推迟函数的执行,直到未来的某个毫秒数。

考虑以下代码:

scss 复制代码
setTimeout(a, 1000);
setTimeout(b, 1001);

人们会期望函数b会在函数a之后执行。然而,这并不能保证-a可能在b之后执行,或者反过来。

现在,考虑以下代码片段中存在的微妙差异:

scss 复制代码
setTimeout(a, 1000);
setTimeout(b, 1000);

在这种情况下,ab的执行顺序是可以预测的。Node 基本上维护一个对象映射,将具有相同超时长度的回调分组。Isaac Schlueter ,Node 项目的前任领导,现任 npm Inc.的首席执行官,这样说: 正如我们在groups.google.com/forum/#!msg/nodejs-dev/kiowz4iht4Q/T0RuSwAeJV0J上发现的,"Node 为每个超时值使用单个低级定时器对象。如果为单个超时值附加多个回调,它们将按顺序发生,因为它们位于队列中。但是,如果它们位于不同的超时值上,那么它们将使用不同的线程中的定时器,因此受[CPU]调度程序的影响。"

在相同的执行范围内注册的定时器回调的顺序并不能在所有情况下可预测地决定最终的执行顺序。此外,超时的最小等待时间为一毫秒。传递零、-1 或非数字的值将被转换为这个最小值。

要取消超时,请使用clearTimeout(timerReference)

setInterval

有许多情况可以想象到定期执行函数会很有用。每隔几秒轮询数据源并推送更新是一种常见模式。每隔几毫秒运行动画的下一步是另一种用例,还有收集垃圾。对于这些情况,setInterval是一个很好的工具:

ini 复制代码
let intervalId = setInterval(() => { ... }, 100);

每隔 100 毫秒,发送的回调函数将执行,这个过程可以使用clearInterval(intervalReference)来取消。

不幸的是,与setTimeout一样,这种行为并不总是可靠的。重要的是,如果系统延迟(比如一些糟糕的写法的阻塞while循环)占据事件循环一段时间,那么在这段时间内设置的间隔将在堆栈上排队等待结果。当事件循环变得不受阻塞并解开时,所有间隔回调将按顺序被触发,基本上是立即触发,失去了它们原本意图的任何时间延迟。

幸运的是,与基于浏览器的 JavaScript 不同,Node 中的间隔通常更加可靠,通常能够在正常使用场景中保持预期的周期性。

unref 和 ref

一个 Node 程序没有理由保持活动状态。只要还有等待处理的回调,进程就会继续运行。一旦这些被清除,Node 进程就没有其他事情可做了,它就会退出。

例如,以下愚蠢的代码片段将使 Node 进程永远运行:

ini 复制代码
let intervalId = setInterval(() => {}, 1000);

即使设置的回调函数没有任何有用或有趣的内容,它仍然会被调用。这是正确的行为,因为间隔应该一直运行,直到使用clearInterval停止它。

有一些情况下,使用定时器来对外部 I/O、某些数据结构或网络接口进行一些有趣的操作,一旦这些外部事件源停止发生或消失,定时器本身就变得不必要。通常情况下,人们会在程序的其他地方捕获定时器的无关状态,并从那里取消定时器。这可能会变得困难甚至笨拙,因为现在需要不必要地纠缠关注点,增加了复杂性。

unref方法允许开发人员断言以下指令:当这个定时器是事件循环处理的唯一事件源时,继续终止进程。

让我们将这个功能测试到我们之前的愚蠢示例中,这将导致进程终止而不是永远运行:

ini 复制代码
let intervalId = setInterval(() => {}, 1000);
intervalId.unref();

请注意,unref是启动定时器时返回的不透明值的一个方法,它是一个对象。

现在,让我们添加一个外部事件源,一个定时器。一旦这个外部源被清理(大约 100 毫秒),进程将终止。我们向控制台发送信息来记录发生了什么:

javascript 复制代码
setTimeout(() => {
  console.log("now stop");
}, 100);

let intervalId = setInterval(() => {
  console.log("running")
}, 1);

intervalId.unref();

你可以使用ref将定时器恢复到正常行为,这将撤消unref方法:

ini 复制代码
let intervalId = setInterval(() => {}, 1000);
intervalId.unref();
intervalId.ref();

列出的进程将继续无限期地进行,就像我们最初的愚蠢示例一样。

快速测验!运行以下代码后,日志消息的预期顺序是什么?

javascript 复制代码
const fs = require('fs');
const EventEmitter = require('events').EventEmitter;
let pos = 0;
let messenger = new EventEmitter();

// Listener for EventEmitter
messenger.on("message", (msg) => {
  console.log(++pos + " MESSAGE: " + msg);
});

// (A) FIRST
console.log(++pos + " FIRST");

//  (B) NEXT
process.nextTick(() => {
  console.log(++pos + " NEXT")
})

// (C) QUICK TIMER
setTimeout(() => {
  console.log(++pos + " QUICK TIMER")
}, 0)

// (D) LONG TIMER
setTimeout(() => {
  console.log(++pos + " LONG TIMER")
}, 10)

// (E) IMMEDIATE
setImmediate(() => {
  console.log(++pos + " IMMEDIATE")
})

// (F) MESSAGE HELLO!
messenger.emit("message", "Hello!");

// (G) FIRST STAT
fs.stat(__filename, () => {
  console.log(++pos + " FIRST STAT");
});

// (H) LAST STAT
fs.stat(__filename, () => {
  console.log(++pos + " LAST STAT");
});

// (I) LAST
console.log(++pos + " LAST");

这个程序的输出是:

erlang 复制代码
FIRST (A).
MESSAGE: Hello! (F).
LAST (I).
NEXT (B).
QUICK TIMER (C).
FIRST STAT (G).
LAST STAT (H).
IMMEDIATE (E).
LONG TIMER (D).

让我们分解上述代码:

A、F 和 I 在主程序流中执行,因此它们将在主线程中具有第一优先级。这是显而易见的;你的 JavaScript 按照它们被编写的顺序执行指令,包括发出回调的同步执行。

主调用堆栈耗尽后,事件循环现在几乎可以开始处理 I/O 操作。这是nextTick请求被执行的时刻,它们排在事件队列的最前面。这时 B 被显示出来。

其余的顺序应该是清楚的。定时器和 I/O 操作将被处理(C、G、H),然后是setImmediate回调的结果(E),始终在执行任何 I/O 和定时器响应之后到达。

最后,长时间超时(D)到达,这是一个相对遥远的未来事件。

请注意,重新排列此程序中的表达式不会改变输出顺序,除了可能重新排列 STAT 结果之外,这只意味着它们以不同的顺序从线程池返回,但仍然作为与事件队列相关的正确顺序的一组。

总结

还没写完,这章省略....

相关推荐
橘右溪11 小时前
Node.js种os模块详解
node.js
HelloRevit12 小时前
npm install 版本过高引发错误,请添加 --legacy-peer-deps
前端·npm·node.js
Bl_a_ck13 小时前
npm、nvm、nrm
前端·vue.js·npm·node.js·vue
zhu_zhu_xia13 小时前
npm包管理工具理解
前端·npm·node.js
eason_fan14 小时前
解决nvm安装指定版本node失败的方法
前端·node.js
全栈派森15 小时前
Web认证宇宙漫游指南
python·node.js
夏虫不与冰语15 小时前
nvm切换node版本后,解决npm找不到的问题
node.js
冰墩墩116 小时前
使用nvm install XXX 下载node版本时网络不好导致npm下载失败解决方案
前端·npm·node.js
techdashen16 小时前
性能比拼: Node.js vs Go
开发语言·golang·node.js
Mintopia17 小时前
Node.js 对前端技术有利的知识点
前端·javascript·node.js