[译] 你不知道的 Node

本文探讨了Node.js运行时的关键概念,如调用栈、事件循环等,并解答了一系列技术问题,帮助开发者深入理解Node.js的工作原理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在今年的 Forward.js 大会(一个 JavaScript 峰会),我进行了一场主题为“你不知道的 Node” 的演讲,在那场演讲中,我问了现场观众一系列关于 Node.js 运行时的问题,然而大部分搞技术的听众都不能全部回答得上。

我当时并没有真的计算过,直到演讲完了才有一些勇敢的人过来跟我坦白说他们不会。

这个问题正是让我发表演讲的原因,我并不认为我们教授 Node 的方式是对的。大多数关于 Nodejs 的教材内容主要集中在 Node 包和 Node 运行时之外的地方,大多数这些包都在 Node 运行时封装好了模块(例如 httpstream),问题可能是藏在运行时里面,然而你不懂 Node 运行时的话,你就麻烦了。

问题:大多数关于 Nodejs 的教材内容主要集中在 Node 包和 Node 运行时之外的地方。

我挑选了几个问题并组织了一些答案来写成这篇文章,答案就在问题的下面,建议尝试先自己回答。

如果你发现了错误或误导性的回答,请跟我联系。

问题 #1:什么是调用栈?它是 V8 的一部分吗?

调用栈百分之百就是 V8 的一部分,它是 V8 用来追踪方法调用的数据结构。每一次我们调用一个方法,V8 在调用栈中放置一个该方法的引用,并且 V8 对每个其他方法的嵌套调用也这样操作,同时也包括那些自身递归调用的方法。

Screenshot captured from my Pluralsight course — Advanced Node.js

当方法的嵌套调用结束时,V8 会逐个地将方法从栈中 pop 出来,并在它的位置使用方法的返回值。

为什么这对于理解 Node 是如此关键?因为在每个 Node 进程中你只有一个调用栈。如果你令调用栈处于忙碌,你整个的 Node 进程也将变得忙碌。牢记这一点!

问题 #2:什么是事件循环?它是 V8 的一部分吗?

你觉得事件循环在这张图的哪个部分?

Screenshot captured from my Pluralsight course — Advanced Node.js

答案是 libuv 。事件循环不是 V8 的一部分!

事件循环是操控外部事件并将它们转换为回调调用的实体,它是从事件队列中取出事件并将事件的回调函数推进调用栈的一个循环。并且该循环过程中分为多个独立的阶段。

如果这是你第一次听说事件循环,这些概念对你可能帮助不大。事件循环是一副很大的轮廓图的其中一部分:

Screenshot captured from my Pluralsight course — Advanced Node.js

你需要先理解这幅轮廓图再理解事件循环,你需要先理解 V8 在这里面饰演的角色、理解 Node APIs 并知道事件是怎样进入队列并被 V8 处理的。

Node APIs 是像 setTimeoutfs.readFile的一些方法,它们不是 JavaScript 本身的一部分,它们就是 Node 提供的方法。

事件循环在这张图片的中间(一个更复杂的版本,真的)饰演一个组织者的角色。当 V8 调用栈为空的时候,事件循环可以决定接下来执行什么。

问题 #3:当调用栈和事件循环队列都为空时,Node 会做什么?

Node 会直接退出。

当你执行一个 Node 程序时,Node 会自动地开始事件循环,当没有事件处理时并且没有其他任务时,Node 则会退出进程。

为了保持一个 Node 进程持续运行,你需要把一些任务放入事件队列中。例如,当你创建一个计时器或一个 HTTP 服务器时,你基本上就是在告诉事件循环要保持并检测这些任务持续执行。

问题 #4:除了 V8 和 Libuv,Node 还有哪些外部依赖?

以下是一个 Node 进程可以使用的所有外部的库:

  • http-parser
  • c-ares
  • OpenSSL
  • zlib

对 Node 本身来说,上面这些库都是外部的,这些库都有自己的源代码、许可证,Node 只是使用它们而已。

你想记住它们是因为你想知道你的程序执行到哪里了,如果你在做一些数据压缩的工作,有可能是在 zlib 这个库遇到问题,Node 是无辜的。

问题 #5:不用 V8 有可能运行一个 Node 进程吗?

这可能是一个奇技淫巧的问题。你肯定是需要一个虚拟机去执行 Node 进程,但 V8 并不是唯一的虚拟机,你还可以使用 Chakra。

查看这个 Github 仓库来跟踪 node-chakra 项目的进度:

问题 #6:module.exports 和 exports 两者的区别?

你可以使用 module.exports 导出模块的 API,你也可以使用 exports,但有个值得注意的地方:

module.exports.g = ...  // Ok

exports.g = ...         // Ok

module.exports = ...    // Ok

exports = ...           // Not Ok复制代码

为什么?

exports 只是一个对 module.exports 的引用或别名,当你修改 exports 时你其实是在无意中试图修改 module.exports,但修改对官方 API (即 module.exports)不会产生影响,你只是在模块作用域中得到一个局部变量。

问题 #7:为什么顶层变量不是全局变量?

如果你在 module1 定义了一个顶层变量 g

// module1.js

var g = 42;复制代码

而你在 module2 依赖 module1并试图访问这个变量 g,你会得到错误 g is not defined

为什么? 如果你在浏览器执行相同的操作,你可以在所有脚本中访问顶层定义的变量。

每个 Node 文件在背后都有自己的 IIFE(立即调用函数表达式),所有在一个 Node 文件中声明的变量都被限制在这个 IIFE 的作用域中。

相关问题: 在一个 Node 文件中只有下面这一行代码,执行它会输出什么:

// script.js</pre>

console.log(arguments);复制代码

你会看到一些参数!

为什么?

因为 Node 执行的是一个函数。Node 将你的代码包裹在一个函数中,这个函数明确地定义了你上面看到的那 5 个参数。

问题 #8:exportsrequire、和 module三个对象在每个文件中都是全局可用的,但他们在每个文件中又有区别,为什么呢?

当你需要使用 require 对象时,你只是像使用全局变量那样直接使用它,然而,如果你在 2 个不同的文件中比较 require 对象的区别,你会发现 2 个不同的对象,怎么回事?

还是因为一样的原因 IIFE(立即调用函数表达式):

正如你所见,IIFF 将以下 5 个参数传递到你的代码中:exports, require, module, __filename, and __dirname

当你在 Node 中使用这 5 个变量的时候似乎是在使用全局变量,但它们只是函数参数。

问题 #9: Node 中的循环依赖是什么?

如果你有一个 module1 依赖于 module2,而 module2 又反过来依赖于 module1,这将发生什么?一个错误?

// module1

require('./module2');

// module2

require('./module1');复制代码

放心,不会报错,Node 允许这样做。

所以,module1 依赖于 module2,但因为 module2 又依赖于 module1,然而 module1 此时还没就绪,module1 只会得到 module2 的不完整版本。

系统已经发出警告了。

问题 #10:什么时候适合使用文件系统的同步方法(像 readFileSync)?

每个 Node 中的 fs 方法都有一个同步版本,为什么你要使用一个同步方法而不是一个异步方法?

有时使用同步方法挺好的,举个例子,可以在服务器还在一直加载的时候,将同步方法用到任何初始化工作中。通常情况下,在初始化工作完成之后,你接下来的工作是根据获得的数据继续进行作业而不是引入回调级别。使用同步方法是可以接受的,只要你使用的同步方法是一次性的。

然而,如果你在一个像是 HTTP 服务器的 on-request 回调函数里使用同步方法,那就真的是 100% 错误!别那样做。

我希望你能答上一部分或者所有的问题,以下是我写得比较深入 Node.js 细节的文章:


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOSReact前端后端产品设计 等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

<think>好的,我需要解决用户遇到的C++编错误,找到geometry_msgs/msg/twist.hpp文件。首先,用户的问题是在编时出现fatal error,提示该头文件存在。这通常是因为编器无法找到相关的ROS消息包。 首先,我应该检查用户是否已经安装了geometry_msgs包。在ROS中,geometry_msgs是标准消息包之一,通常属于ros-<distro>-common-msgs这个包。用户可能需要确认是否已经正确安装。例如,在Ubuntu下,可以使用sudo apt-get install ros-<distro>-common-msgs来安装,其中<distro>是ROS版本,比如noetic或foxy。 接下来,用户的CMakeLists.txt配置是否正确也很重要。需要确认find_package(catkin REQUIRED COMPONENTS ...)中是否包含了geometry_msgs。如果没有,添加进去并重新运行catkin_make。 另外,可能涉及到依赖项的问题。如果项目依赖其他包,需要在package.xml中添加<depend>geometry_msgs</depend>,以确保构建系统知道需要这些依赖。 如果用户最近添加了新的消息或服务,可能需要先单独编相关的消息包,比如先进入工作空间的src目录,克隆或创建相应的消息包,然后回到工作空间运行catkin_make。此外,环境变量是否正确设置,比如source devel/setup.bash,确保终端会话能找到新生成的头文件路径。 还有可能是文件路径或名称错误,需要检查include语句是否正确,比如是否应该使用#include <geometry_msgs/Twist.h>而是带有msg目录的路径,这取决于ROS版本和生成方式的同。例如,ROS1和ROS2的头文件路径结构有所同,用户可能混淆了这两者。 最后,建议用户清理构建并重新编,有时候旧的构建文件可能导致问题。运行catkin clean或者删除build和devel目录后重新编可能解决问题。 需要综合考虑以上可能性,逐步引导用户检查和修正,确保每个步骤正确执行,从而解决头文件找到的问题。</think>针对C++编报错`geometry_msgs/msg/twist.hpp: No such file or directory`,请按以下步骤排查: 1. **安装依赖包** 确认已安装ROS的geometry_msgs包,执行: ```bash sudo apt-get install ros-<distro>-common-msgs # 如ros-noetic-common-msgs ``` 2. **检查CMakeLists配置** 在`CMakeLists.txt`中确保包含geometry_msgs: ```cmake find_package(catkin REQUIRED COMPONENTS geometry_msgs roscpp ) include_directories(${catkin_INCLUDE_DIRS}) ``` 3. **验证package.xml依赖** 在`package.xml`中添加: ```xml <depend>geometry_msgs</depend> ``` 4. **检查头文件引用方式** - **ROS1**使用标准路径格式: ```cpp #include <geometry_msgs/Twist.h> ``` - **ROS2**需要显式包含命名空间: ```cpp #include <geometry_msgs/msg/twist.hpp> ``` 5. **重新生成消息文件** 若涉及自定义消息,先单独编消息包: ```bash cd ~/catkin_ws && catkin_make --pkg geometry_msgs ``` 6. **更新环境变量** 编后执行环境配置: ```bash source devel/setup.bash ``` [^1]: 引用自编环境配置问题解决方案 [^2]: 参考ROS-Arduino集成案例的依赖安装流程
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值