Node.js 底层原理 #1 - 了解我们的工具

2025-05-28

Node.js 底层原理 #1 - 了解我们的工具

最近,我被邀请在巴西的一次大型会议“The Conf”上发表演讲

会议的重点是用英语创建内容,以便其他人将来可以通过在线观看录制的演讲而受益,而不仅仅是讲葡萄牙语的巴西人。

您可以在下面的视频中观看本系列

我觉得我之前的演讲内容不够深入,不够先进。所以我决定写一篇关于 Node.js、JavaScript 以及整个 Node.js 生态系统实际工作原理的演讲。这是因为大多数程序员只是使用一些工具,却从来不知道它们到底能做什么,或者它们是如何工作的。

在当今世界,这“很好”,我们有很多库,无需再阅读书籍,也无需阅读更多有关处理器架构的书籍,因此我们可以用汇编语言编写一个简单的时钟。然而,这让我们变得非常懒惰,在不了解的情况下使用这些东西,造成了一种氛围,每个人都只阅读足够多的资料来创建他们需要的东西,而忘记了随之而来的所有概念。毕竟,复制粘贴 Stack Overflow 代码要容易得多。

因此,考虑到这一点,我决定深入研究 Node.js 的内部结构,至少展示事物是如何粘合在一起的,以及我们的大多数代码实际上是如何在 Node.js 环境中运行的。

这是关于这个特定主题的几篇文章中的第一篇,我为了准备我的演讲整理并研究了这些文章。由于内容太多,我不会在这篇文章中发布所有参考资料。相反,我会将全部内容分成几篇文章,每篇文章涵盖研究的一部分,最后一篇文章中,我会发布参考资料和我演讲的幻灯片。

希望大家喜欢它:D

目标

本系列文章的目标是帮助读者理解 Node.js 的内部工作原理。这主要是因为 Node.js 和 JavaScript 虽然凭借其丰富的库闻名全球,但实际上却鲜有人知其底层工作原理。为此,我们将尝试涵盖以下几个主题:

  1. 什么是 Node.js
    1. 简史
    2. JavaScript 本身的简史
    3. Node.js 的元素
  2. 执行 I/O 文件读取函数调用
  3. JavaScript
    1. 它的内部是如何工作的?
      1. 调用堆栈
    2. 内存分配
  4. Libuv
    1. 什么是 libuv?
    2. 我们为什么需要它?
    3. 事件循环
    4. 微任务和宏任务
  5. V8
    1. 什么是v8
    2. 概述
      1. 使用 Esprima 的抽象语法树
    3. 旧的编译管道
      1. 完整的代码生成
      2. 曲轴
    4. 新的编译管道
      1. 点火
      2. 涡轮风扇
        1. 隐藏类和变量分配
    5. 垃圾收集
  6. 编译器优化
    1. 常量折叠
    2. 归纳变量分析
    3. 重新物质化
    4. 删除递归
    5. 森林砍伐
    6. 窥孔优化
    7. 内联扩展
    8. 内联缓存
    9. 死代码消除
    10. 代码块重新排序
    11. 跳转线程
    12. 蹦床
    13. 公共子表达式消除

什么是 Node.js

Node.js 由其创始人 Ryan Dahl 定义为“一组运行在 V8 引擎之上的库,让我们能够在服务器上运行 JavaScript 代码”,维基百科将其定义为“一个开源的、跨平台的 JavaScript 运行时环境,可以在浏览器之外执行代码”。

本质上,Node.js 是一个允许我们在浏览器域之外执行 JS 的运行时。然而,这并不是服务器端 JavaScript 的第一个实现。1995 年,Netscape 实现了所谓的 Netscape Enterprise Server,它允许用户在服务器中运行 LiveScript(早期的 JavaScript)。

Node.js简史

Node.js 最初发布于 2009 年,由 Ryan Dahl 编写,后来由 Joyent 赞助。该运行时的起源源于 Apache HTTP Server(当时最流行的 Web 服务器)处理大量并发连接的能力有限。此外,Dahl 还批评了 Node.js 的顺序式代码编写方式,这可能导致整个进程阻塞,或者在多个并发连接的情况下产生多个执行堆栈。

Node.js 于 2009 年 11 月 8 日首次在 JSConf EU 上亮相。它结合了 V8、由最近编写的 libuv 提供的事件循环以及低级 I/O API。

JavaScript 简史

JavaScript 被定义为一种符合 ECMAScript 规范并由 TC39 维护的“高级解释型脚本语言”。JS 由 Brendan Eich 于 1995 年创建,当时他正在为 Netscape 浏览器开发一种脚本语言。JavaScript 的创建完全是为了实现 Marc Andreessen 的理念:在 HTML 和网页设计师之间建立一种“胶水语言”,这种语言应该易于组装图像和插件等组件,代码可以直接写在网页标记中。

Brendan Eich 最初被招募来将 Scheme 语言实现到 Netscape 中,但由于 Sun Microsystems 和 Netscape 合作,希望将 Java 纳入 Netscape Navigator,他的工作重点转向了创建一种与 Java 语法相似的语言。为了捍卫 JavaScript 的理念,对抗其他提案,Eich 用 10 天时间编写了一个可运行的原型。

ECMA 规范于一年后问世。当时,Netscape 将 JavaScript 语言提交给 ECMA 国际组织,希望制定一个标准规范,以便其他浏览器厂商能够基于 Netscape 的工作成果进行实现。这促成了 1997 年第一个 ECMA-262 标准。ECMAScript-3 于 1999 年 12 月发布,它是 JavaScript 语言的现代基准。ECMAScript 4 被搁置,因为微软无意与 IE 合作或在 IE 中实现合适的 JavaScript,尽管他们没有竞争性的提案,并且在服务器端对 .NET 语言进行了部分但存在分歧的实现。

2005年,开源社区和开发者社区开始致力于彻底革新JavaScript的使用方式。首先,2005年,Jesse James Garrett发布了后来被称为AJAX的草案,这引发了JavaScript使用的复兴,由jQuery、Prototype和MooTools等开源库引领。2008年,在整个社区重新开始使用JS之后,ECMAScript 5于2009年发布并正式发布。

组成 Node.js 的元素

Node.js 由几个依赖项组成:

  • V8
  • Libuv
  • http解析器
  • c-战神
  • OpenSSL
  • zlib

这张图片有完美的解释:

摘自 Samer Buna 的 Pluralsight 课程:高级 Node.js

话虽如此,我们可以将 Node.js 分为两部分:V8 和 Libuv。V8 大约由 70% 的 C++ 和 30% 的 JavaScript 组成,而 Libuv 几乎完全由 C 语言编写。

我们的示例 - I/O 函数调用

为了实现我们的目标(并明确接下来要做的事情),我们将从编写一个简单的程序开始,该程序读取文件并将其打印到屏幕上。您会发现,这段代码并非程序员所能编写的最佳代码,但它足以作为我们后续所有部分的研究对象。

如果你仔细查看Node.js 源代码,你会注意到两个主要文件夹:libsrclib文件夹包含我们项目所需的所有函数和模块的JavaScriptsrc定义。文件夹包含它们附带的C++ 实现fs,Libuv 和 V8 也位于此处,所有模块(例如http、等)的实现也crypto都位于此处。

假设有这个简单的程序:



const fs = require('fs')
const path = require('path')
const filePath = path.resolve(`../myDir/myFile.md`)

// Parses the buffer into a string
function callback (data) {
  return data.toString()
}

// Transforms the function into a promise
const readFileAsync = (filePath) => {
  return new Promise((resolve, reject) => {
    fs.readFile(filePath, (err, data) => {
      if (err) return reject(err)
      return resolve(callback(data))
    })
  })
}

(() => {
  readFileAsync(filePath)
    .then(console.log)
    .catch(console.error)
})()


Enter fullscreen mode Exit fullscreen mode

是的,我知道有util.promisifyfs.promises,但是,我想手动将回调转换为承诺,以便我们能够更好地理解事情的实际运作方式。

本文中所有示例都与该程序相关。这是因为该函数既不fs.readFileV8 引擎的一部分,也不是 JavaScript 的一部分。该函数由 Node.js 单独实现,作为与本地操作系统的 C++ 绑定,然而,我们使用的高级 API完全由 JavaScript 实现,JavaScript 会调用这些绑定。以下是该函数的完整源代码(因为整个文件长达 1850 行,但它在参考文献中):fs.readFile(path, cb)readFile



// https://github.com/nodejs/node/blob/0e03c449e35e4951e9e9c962ff279ec271e62010/lib/fs.js#L46
const binding = internalBinding('fs');
// https://github.com/nodejs/node/blob/0e03c449e35e4951e9e9c962ff279ec271e62010/lib/fs.js#L58
const { FSReqCallback, statValues } = binding;

// https://github.com/nodejs/node/blob/0e03c449e35e4951e9e9c962ff279ec271e62010/lib/fs.js#L283
function readFile(path, options, callback) {
  callback = maybeCallback(callback || options);
  options = getOptions(options, { flag: 'r' });
  if (!ReadFileContext)
    ReadFileContext = require('internal/fs/read_file_context');
  const context = new ReadFileContext(callback, options.encoding);
  context.isUserFd = isFd(path); // File descriptor ownership

  const req = new FSReqCallback();
  req.context = context;
  req.oncomplete = readFileAfterOpen;

  if (context.isUserFd) {
    process.nextTick(function tick() {
      req.oncomplete(null, path);
    });
    return;
  }

  path = getValidatedPath(path);
  binding.open(pathModule.toNamespacedPath(path),
               stringToFlags(options.flag || 'r'),
               0o666,
               req);
}


Enter fullscreen mode Exit fullscreen mode

免责声明:我将代码引用粘贴到 Github 源链接中,截至0e03c449e35e4951e9e9c962ff279ec271e62010目前提交的是最新的,这样,该文档将始终指向我编写时的正确实现。

看到第 5 行了吗?我们 require 了read_file_context另一个 JS 文件(引用中也有)。在fs.readFile 源代码的末尾,我们调用了binding.open,这是一个 C++ 调用,用于打开一个文件描述符,传递路径、C++ 标志fopen、八进制格式的文件模式权限(0o 在 ES6 中是八进制)以及最后一个req变量,该变量是异步回调函数,用于接收文件上下文。

除此之外,我们还有internalBinding,它是私有的内部 C++ 绑定加载器,最终用户(比如我们)无法访问它,因为它们是通过 访问的。它实际上负责加载 C++ 代码。而这正是我们非常依赖 V8 的NativeModule.require地方

因此,基本上,在上面的代码中,我们需要fs与 进行绑定internalBinding('fs'),它调用并加载包含我们的和函数的所有 C++ 实现的文件src/node_file.cc(因为整个文件都在 中) namespace fsFSReqCallbackstatValues

该函数FSReqCallback是我们调用时使用的异步回调fs.readFile(当我们使用时,fs.readFileSync有另一个函数被调用,该函数在这里FSReqWrapSync定义),并且它的所有方法和实现都在这里定义,并在这里作为绑定公开



// https://github.com/nodejs/node/blob/0e03c449e35e4951e9e9c962ff279ec271e62010/src/node_file.cc

FileHandleReadWrap::FileHandleReadWrap(FileHandle* handle, Local<Object> obj)
  : ReqWrap(handle->env(), obj, AsyncWrap::PROVIDER_FSREQCALLBACK),
    file_handle_(handle) {}

void FSReqCallback::Reject(Local<Value> reject) {
  MakeCallback(env()->oncomplete_string(), 1, &reject);
}

void FSReqCallback::ResolveStat(const uv_stat_t* stat) {
  Resolve(FillGlobalStatsArray(env(), use_bigint(), stat));
}

void FSReqCallback::Resolve(Local<Value> value) {
  Local<Value> argv[2] {
    Null(env()->isolate()),
    value
  };
  MakeCallback(env()->oncomplete_string(),
               value->IsUndefined() ? 1 : arraysize(argv),
               argv);
}

void FSReqCallback::SetReturnValue(const FunctionCallbackInfo<Value>& args) {
  args.GetReturnValue().SetUndefined();
}

void NewFSReqCallback(const FunctionCallbackInfo<Value>& args) {
  CHECK(args.IsConstructCall());
  Environment* env = Environment::GetCurrent(args);
  new FSReqCallback(env, args.This(), args[0]->IsTrue());
}

// Create FunctionTemplate for FSReqCallback
Local<FunctionTemplate> fst = env->NewFunctionTemplate(NewFSReqCallback);
fst->InstanceTemplate()->SetInternalFieldCount(1);
fst->Inherit(AsyncWrap::GetConstructorTemplate(env));
Local<String> wrapString =
    FIXED_ONE_BYTE_STRING(isolate, "FSReqCallback");
fst->SetClassName(wrapString);
target
    ->Set(context, wrapString,
          fst->GetFunction(env->context()).ToLocalChecked())
    .Check();


Enter fullscreen mode Exit fullscreen mode

在最后一部分,有一个构造函数定义:Local<FunctionTemplate> fst = env->NewFunctionTemplate(NewFSReqCallback)。这基本上表明,当我们调用时,将会调用。现在看看属性是如何new FSReqCallback()出现部分中的,以及在和上是如何定义和使用的NewFSReqCallbackcontexttarget->Set(context, wrapString, fst->GetFunction)oncomplete::Reject::Resolve

值得注意的是,该req变量是基于调用结果构建的new ReadFileContext,其引用方式为context,设置方式为req.context。这意味着该req变量也是用函数构建的请求回调的 C++ 绑定表示FSReqCallback(),并将其上下文设置为我们的回调并监听oncomplete事件。

结论

目前我们还没有看到太多内容。不过,在后续的文章中,我们将深入探讨它的实际工作原理,以及如何使用我们的函数来更好地理解我们的工具!

再见!

文章来源:https://dev.to/_staticvoid/node-js-under-the-hood-1-getting-to-know-our-tools-1465
PREV
Node.js 底层原理 #2 - 理解 JavaScript
NEXT
Node.js 由 Baixo dos Panos #1 - Conhecendo nossas ferramentas