Node.js、express 和对象实例化内部/外部路由端点

dvs*_*kup 3 javascript node.js promise ecmascript-6

我有一个关于对象实例化的问题。现在我已经设置了一个相当大的应用程序,并在后端实现了 RESTful 端点。

我想知道对象实例化的“最佳”方式是什么,以及垃圾收集器如何受到我所做的影响。

以下面为例:

const MyClass = require('../controllers/myClassController');
const router = require('express').Router();

router.get('/', (req, res) => {
    console.log('GET request at api/someEndPoint');
    
    const myClass = new MyClass();
    myClass.doSomePromise()
        .then(() => {
            res.end();
        })
        .catch((err) => {
            res.status(500).end();
        });
});
Run Code Online (Sandbox Code Playgroud)

这里,一个对象在路由路径实现内部实例化。

现在,以此为例:

const MyClass = require('../controllers/myClassController');
const router = require('express').Router();

const myClass = new MyClass();

router.get('/', (req, res) => {
    console.log('GET request at api/someEndPoint');
    
    myClass.doSomePromise()
        .then(() => {
            res.end();
        })
        .catch((err) => {
            res.status(500).end();
        });
});
Run Code Online (Sandbox Code Playgroud)

这里,该对象是在路由路径实现之外实例化的。在我看来,这似乎会在应用程序的生命周期中无限期地持续存在,而第一个示例中,变量myClass将被清除。

所以我真的想知道:

  1. 我以正确的方式思考这个问题吗?

  2. 我应该使用一种方法而不是另一种方法有理由吗?似乎如果我选择选项 2,我还不如static在类本身上创建方法......

  3. 每种方法的一般优点/缺点是什么?

    我觉得在我的脑海里,我正在与事情没有得到正确清理进行斗争,在流量多少、避免竞争条件等方面,一种方法比另一种方法更好!

jfr*_*d00 6

这真的很简单。

  1. 如果您在整个应用程序的持续时间内只需要一个对象,并且可以将该一个对象用于任何恰好需要使用它的请求,那么它就是一种“应用程序周期”类型的对象。在初始化时创建一个,存储其实例(可能作为模块级变量)并从那时起使用它。举个例子,如果您有某种设计供大量请求处理程序使用的缓存对象,那么您希望创建一次,然后保留它并每次由所有请求使用相同的对象使用它的处理程序。作为另一个示例,某些应用程序在启动时创建与其主数据库的一个连接,然后所有其他请求都可以使用该连接。

  2. 如果您需要为每个可能使用它的单个请求提供一个新的对象,请在请求处理程序中创建它,并让正常的垃圾收集在请求完成时负责清理它。例如,如果您需要创建一个Date()对象来测量请求期间的持续时间,最简单的实现将new Date()在请求处理程序中创建一个对象,将其用于您需要的用途,然后让 GC 处理它当请求完成时。

  3. 如果您需要为每个请求一个独立的对象,以便当多个请求处理程序同时“进行中”时,您需要确保每个请求处理程序都有自己的对象,并且没有一个对象被共享或冲突。

  4. 还有混合模型(通常以这种方式完成性能优化),您可以创建一个对象池,然后每个需要对象的请求都可以“签出一个”,使用它一段时间,然后在需要时将其返回到池中完成。当一小部分对象通常可以满足服务器的需求时,这通常可以节省从头开始重建对象的时间。一些数据库使用这种“池”模型进行数据库连接。该请求可能只是从池中获取一个数据库连接,进行查询,然后将连接返回到池中,而不是为每个想要执行查询的请求创建一个新的数据库连接。

在 Node.js 内部,它使用线程池来实现磁盘 I/O 的某些部分。类似的概念。


我想知道对象实例化的“最佳”方式是什么,以及垃圾收集器如何受到我所做的影响。

最好的方法是选择符合用途的实例化模型。在上面的选项 1 中,对象可能会在整个应用程序周期中存储,而根本不会被 GC。在选项 2 和 3 中,将为每个请求创建对象并进行 GC。您不应该担心创建和使用对象然后让它被GC。这就是该语言被设计用于处理没有持久状态并且应该保存更长时间的事物的方式。

我应该使用一种方法而不是另一种方法有理由吗?似乎如果我选择选项 2,我还不如在类本身上创建静态方法......

如果对象中没有特定于请求的状态,并且如果有多个请求处理程序同时尝试使用该对象,则对象本身的状态不会遇到麻烦,那么您实际上就拥有了一个全局服务,可以处理任何请求想要使用它的处理程序可以使用它。如果您将其实现为您创建的对象,存储其实例,然后在该实例上调用传统方法,或者将其实现为实例化自身的单例,并且您只需对其调用函数或静态方法,这取决于您。这只是编码风格的差异,而不是功能的差异。

每种方法的一般优点/缺点是什么?

一般来说,您需要尽可能多的封装和本地状态。无需将特定于请求的内容放入共享全局变量中。为特定请求计算或弄清楚的事情属于请求处理程序逻辑的内部。因此,只需按照上面的四个问题来确定该对象是属于请求处理程序的本地对象还是在更高范围内共享。如果您可以通过保持本地化来实现您的目标,那么通常会更干净、更安全、更灵活。

我觉得在我的脑海里,我正在与事情没有得到正确清理进行斗争,在流量多少、避免竞争条件等方面,一种方法比另一种方法更好!

GC 会为您进行清理工作。您可以信赖这一点。只要您不持久地将对象的引用存储在具有持久作用域的变量中,垃圾收集就会很好地为您清理一切。这就是该语言的设计用途。你不应该害怕它。如果您来自 C 或 C++ 等非 GC 语言,那么您可能会想太多。GC 是您的朋友。非常、非常、非常偶尔,您可能需要进行一些性能调整,以更加了解您要求 GC 执行的操作量,但这几乎不应该是您担心的事情,直到您真正发现自己有一些东西这需要以这种方式进行特殊优化。

避免请求处理程序之间的竞争条件的最简单方法是不在请求处理程序之间共享任何状态。如果处理给定请求所需的所有内容都是在本地创建和使用的,并且从未提供给任何其他请求处理程序,那么您绝对可以避免多个请求处理程序之间的任何竞争或共享条件。一般来说,从这里开始,因为这是最简单、最安全的。然后,如果您发现需要对某些内容进行性能优化,那么您可能会探索某些类型的对象的共享或缓存,并且您必须小心地这样做,以免引入共享问题。但是,您很少会在开始设计时尝试这样做,因为大多数时候不需要额外的复杂性。