随着 Node-RED 1.0 中异步消息传递的改变,我们也在改变一些消息在节点间克隆的方式。这方面的行为并非总是清晰,并且可能对不熟悉 JavaScript 对象处理原则的最终用户导致意外结果。
这篇帖子解释了克隆消息的全部内容、为什么它有必要以及 1.0 中正在发生的变化。
按引用传递
在我们深入了解克隆消息的细节之前,我们再次绕道了解 JavaScript 的工作原理。
让我们考虑以下代码
> let a = { payload: "hello" };
> let b = a;
> b.payload = "goodbye";
> console.log(a)
{ payload: 'goodbye' }
我们创建一个新对象并将其赋值给变量 a
。然后我们将变量 b
赋值为 a
的值。接下来我们改变 b.payload
的值。最后我们打印原始变量 a
。
仿佛魔法一般,我们对变量 b
所做的更改也作用于变量 a
。这是因为我们没有创建对象的副本——我们创建了对内存中同一对象的新引用。
这被称为按引用传递,如果你没有做好准备,它可能会导致意想不到的结果。
克隆消息
在 Node-RED 流程中,当一个节点接收到消息(这是一个 JavaScript 对象)时,它可以随意修改消息,然后将其传递给它连接的任何节点。
当节点 A 发送消息时,它会生成两个“发送事件”——一个给节点 B,一个给节点 C。如果我们将消息简单地传递给节点 B 然后再传递给节点 C,那么节点 B 对消息的任何修改都将对节点 C 可见。
这就是需要克隆消息的原因。Node-RED 在消息传递之前会自动克隆消息,以防止这种类型的跨分支修改。
但这并非那么简单。克隆消息可能代价高昂。对于没有分支的单行节点流程,通常不需要克隆任何消息。
因此,代码会尝试优化何时克隆消息。算法是:
当调用
node.send()
时,它会生成一个发送事件列表。第一个发送事件按原样使用给定的消息对象。所有剩余的发送事件在传递之前都会克隆它们的消息。
本质上这意味着,对于连接到一个其他节点的节点,调用 node.send(msg)
将不会克隆该消息,因为没有必要。
但这个算法有其局限性。特别是,我们无法处理函数节点多次调用 node.send()
并使用相同消息对象的情况。
例如,考虑函数节点中的以下代码:
msg.topic = "A";
msg.payload = "1";
node.send(msg);
msg.topic = "B";
msg.payload = "2";
node.send(msg);
每次对 node.send()
的单独调用都会生成一个发送事件,因此消息不会被克隆。
对于某些流程,这在 Node-RED 1.0 之前并不是问题。如果流程中的后续节点完全是同步的,那么对 node.send()
的第一次调用将完全向下传递,并在执行返回到函数并修改消息以进行第二次调用之前完成。
但是随着 Node-RED 1.0 引入完全异步消息传递,这种代码模式可能会不安全使用。消息将在第一次 node.send()
调用产生的发送事件被传递之前被修改。
这可能会导致非常微妙且难以发现的问题。没有响亮的提示来提醒用户有问题。如果上面的代码连接到 MQTT 节点,它将在主题 B
上生成重复消息,而在主题 A
上则没有任何消息。
我们正在更改 Function 节点使用的克隆默认方法,以防止此问题。
默认克隆
在 Node-RED 1.0 中,当 Function 节点调用 node.send()
时,它现在将克隆每一条消息,包括第一条。这将确保像上面那样的代码能够继续工作。
但不幸的是,这并非没有代价。克隆可能是一项昂贵的操作。对于许多用户来说,这根本不重要——他们的消息相对较小且不频繁。
对于那些拥有非常大的消息、高消息速率以及更关键的是,使用无法克隆的消息的流程,这将是一个更大的问题。例如,以高速率传输视频帧的流程。
对于这些流程,我们正在为 node.send()
引入一个可选的新参数,该参数将保持现有行为:
node.send(msg, false);
这将告诉 Function 节点不要克隆消息——尽管如果流程分支,第二和后续分支仍将获得克隆的消息的规则仍然适用。
为什么要改变默认行为呢?
我们每做一次改变,都必须评估其潜在影响。目标是尽量减少这种影响,并保持流程按用户期望运行。
当这个问题出现时,我们有两种可能的方法。
一种选择是代码不做任何改动,并更新我们的文档以解释您不应该重用消息对象,并使用 RED.util.cloneMessage()
在您的函数代码中手动克隆消息。
这种方法的缺点是它让用户不确定他们应该如何处理他们的流程。我们有大量的用户,他们不是经验丰富的 JavaScript 开发人员,对 Node-RED 的内部工作原理不感兴趣。他们期望他们的流程能正常工作。事实上,由此造成的任何问题都会很微妙且难以追踪,这使得潜在影响非常大。
另一种选择,也是我们选择的,是改变默认行为,以确保大多数用户的流程能继续工作,而无需他们改变任何东西。
这种选择的一个缺点是,有些流程依赖于不克隆的行为。然而,它们现在将以一种更明显的方式失败;克隆将从传递的第一条消息开始失败。这将使识别故障的 Function 节点并根据需要添加 false
参数变得更容易。事实上,他们今天就可以更新他们的流程以添加额外的参数,为升级到 1.0 做准备。
另一个缺点是额外克隆可能造成的性能损失。但同样,轻微的性能损失优于不正确的流程行为。特别是我们可以在未来寻求其他方法来提高整个系统的性能。
考虑到要么以一种微妙且难以检测的方式破坏大量流程,要么对一小部分流程造成明显的中断,我希望你能明白我们为什么选择后者。
其他节点呢?
上述更改仅适用于 Function 节点。自定义节点仍负责在传递给 node.send()
之前根据需要克隆消息。