Node.js、express以及路由端点内/外的对象实例化

3
我有一个关于对象实例化的问题。目前我有一个相当大的应用程序集,后端实现了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();
        });
});

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

现在,以这个为例:

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();
        });
});

在这里,对象是在路由路径实现之外实例化的。在我看来,这将会持续到应用程序的生命周期,而第一个示例中,变量myClass将被清除。
所以,我的疑问是:
1. 我的想法对吗? 2. 为什么我要选择其中一种方法?如果我选择第二种方法,似乎我可以直接在类本身上创建static方法... 3. 每种方法的优缺点是什么?
我觉得自己在脑海中斗争的是,事情没有被正确地清理,其中一种方法比另一种方法更好,涉及到流量大小、避免竞态条件等问题!

1
如果您的每个请求不需要不同的实例,则没有必要重复构建和拆除它们。 - Bergi
2个回答

6

其实很简单。

  1. 如果你只需要一个对象在整个应用程序期间使用,并且可以在任何请求中使用该对象,则它是一种“应用程序生命周期”对象。在初始化时创建一个对象,将其实例存储下来(可能作为模块级变量),然后从那时起始终使用它。例如,如果您有某种缓存对象旨在由许多请求处理程序使用,则这将是希望仅创建一次,然后保留它并由所有使用它的请求处理程序每次使用相同对象的内容。另一个例子是,一些应用程序在启动时向其主数据库创建一个连接,然后所有其他请求都可以使用该连接。

  2. 如果您需要在每个请求中使用一个新对象,则在请求处理程序内部创建它,并让普通垃圾回收程序在请求完成时清理它。例如,如果您需要创建一个Date()对象以便在请求期间测量某些时间持续时间,最简单的实现将在请求处理程序内创建一个new Date()对象,用于您需要使用的目的,然后当请求完成时让GC程序处理它。

  3. 如果您需要每个请求一个独立的对象,以便在同时有多个请求处理程序时,您需要确保每个请求处理程序都具有自己的对象,而没有共享或冲突。

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

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


我想知道最佳对象实例化方式以及我的做法对垃圾收集器的影响。

最佳方法是选择与用途相匹配的实例化模型。在上述第一种选项中,对象可能会被存储整个应用程序周期并且根本不被GC。在第二和第三个选项中,对象将为每个请求创建并被GC。您不应该担心为使用而创建对象,然后让它GC。对于没有持久性的状态并且应该长时间保留的内容,这就是语言的设计工作方式。

有没有理由让我选择一种方法而不是另一种方法?如果我选择选项2,那么我可能会直接在类本身上创建静态方法... 如果对象中没有请求特定状态,并且对象本身的状态不会因为有多个请求处理程序同时使用该对象而出现问题,那么您真的只有一个全局服务,任何想要使用它的请求处理程序都可以使用它。你可以将其实现为你创建的对象,存储该实例并调用该实例上的传统方法,也可以将其实现为单例,该单例实例化自己,然后只需在其上调用函数或静态方法。这只是一种编码风格的区别,而不是功能上的区别。
每种方法的一般优缺点是什么?
通常,您希望尽可能多地封装和本地状态。不需要将特定于请求的东西放入共享全局变量中。计算或为特定请求找到的东西属于请求处理程序逻辑的内部。因此,只需按照上面的四个问题确定对象是否属于请求处理程序本地或共享更高范围即可。如果您可以通过保持本地事物来实现目标,那么这通常更清洁,更安全,更灵活。
我觉得我的头脑中正在进行的斗争是,事情没有被正确清理,一种方法比另一种方法更好,涉及到流量有多大,避免竞态条件等等!
GC会为您处理清理工作。你可以依靠它。只要您不在具有持久范围的变量中持久存储对对象的引用,垃圾回收将为您很好地清除事物。这就是语言设计的使用方式。您不应该害怕它。如果您来自像C或C++这样的非GC语言,则可能会过度思考此问题。GC在那里成为你的朋友。非常非常非常偶尔,您可能需要执行一些性能调整,以更加了解您要求GC进行多少操作,但这基本上几乎永远不会成为您担心的事情,直到您实际发现需要以这种方式进行特殊优化的东西。
避免请求处理程序之间的竞争条件的最简单方法是不在请求处理程序之间共享任何状态。如果您处理给定请求所需的所有内容都是本地创建和使用的,并且从未提供给任何其他请求处理程序,则绝对会避免多个请求处理程序之间的任何竞争或共享条件。通常,从那里开始,因为那是最简单和最安全的。然后,如果您发现有些东西需要进行性能优化,那么您可以探索某些类型对象的共享或缓存,您必须小心地进行操作,以避免引入共享问题。但是,您很少从设计开始尝试这样做,因为大多数时间不需要这种额外的复杂性。

非常感谢您提供如此详细的回复!我真的很感激! - dvsoukup

0
  1. 是的,你的想法是正确的。尽管像对象实例化这样的东西可能只是微小的优化。

  2. 正如Bergi在评论中提到的,如果不需要,你不应该在路由内部重新构建或处理任何东西。你必须时刻记住,路由处理程序内部的代码会在每个到达此路由的请求上执行。

  3. 所以你实际上正在问的是:我应该在应用程序生命周期内一次性创建const myClass = new MyClass();,还是潜在地每秒多达数千次。只有你能回答是否真的需要每次处理此路由的请求时都创建一个新的类实例。很可能你不需要每次创建一个实例。因此,为了避免不必要的工作,请将其放在路由处理程序之外。

至于#3,由于以上原因,没有真正的利弊清单。如果可以避免在路由处理程序内部进行实例化,则应该避免!


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接