requirejs - 多次调用 require 的性能

6
我想知道在具有多个模块的项目中,使用RequireJS的正确方法是什么,涉及到具有较少依赖性的多个require调用与具有所有依赖项的单个require调用的性能方面。假设我需要加载一些模块(gmaps、jquery、module1、module2、module3)来构建一个应用程序,其中一些模块的使用相当独立。因此,问题是推荐以下哪种替代方案(假定这段代码是加载到页面中的主要模块):
require(['gmaps'], function(gmaps){
   gmaps.use();
});

require(['jquery','module1'], function(jquery, module1){
   module1.use();
});

require(['jquery','module2'], function(jquery, module2){
   module2.use();
});

require(['jquery','module3'], function(jquery, module3){
   module3.use();
});

vs

require(['jquery','module1','module1','module1','gmaps'], function(jquery, module1,module2,module3,gmaps){
   module1.use();
   module2.use();
   module3.use();
   gmaps.use();
});

换句话说,require的性能惩罚是什么,哪种方式最佳实践。
3个回答

6
这里的答案是“视情况而定”。我作为一个在大型应用程序中使用过RequireJS的人,但并没有彻底阅读RequireJS代码的人发言。(只是指出了那些对RequireJS内部非常熟悉的人可能会有不同的解释。)require的成本可以分为3种情况:
  1. 如果模块从未被加载,则require从服务器加载文件,执行文件,执行模块的工厂函数,并返回对该模块的引用。(从网络加载文件的成本通常远远超过其他成本。)

  2. 如果模块已经被加载但从未被要求,则require执行模块的工厂函数,并返回对该模块的引用。(这通常会在一个优化后的应用程序中发生。)

  3. 如果模块已经被加载和要求,则require返回对该模块的引用。

成本情况1 > 成本情况2 > 成本情况3。
首先,让我们考虑每个文件一个模块的情况。该应用程序未经过优化。我有一个名为module1的模块,它很少被要求。在主应用程序中使用它可以这样建模:
define(["module1", <bunch of other required modules>],
    function (module1, <bunch of other modules variables>) {

    [...]

    if (rare_condition_happening_once_in_a_blue_moon)
        module1.use();

    [...]
});

即使我没有使用模块,我仍然会始终支付成本场景1的价格。更好的做法是:

define([<bunch of required modules>],
    function (<bunch of module variables>) {

    [...]

    if (rare_condition_happening_once_in_a_blue_moon)
        require(["module1"], function (module1) {
            module1.use();
        });

    [...]
});

这样,我只需要偶尔加载模块,就能避免重复的劳动。

但是,如果我需要反复使用 module 呢?可以将其建模为:

define(["module1", <bunch of other required modules>],
    function (module1, <bunch of other modules variables>) {

    [...]

    for(iterate a gazillion times)
        module1.use();

    [...]
});

在这种情况下,成本方案1只需支付一次即可。如果我像这样使用require:
define([<bunch of required modules>],
    function (<bunch of module variables>) {

    [...]

    for(iterate a gazillion times)
        require(["module1"], function (module1) {
            module1.use();
        });

    [...]
});

我支付一次成本情景1和(gazillion次 - 1)成本情景3。最终,无论module1是否应该包括在define调用的要求中或者单独一个require调用中,都取决于您的应用程序的具体情况。
如果应用程序已经通过使用r.js或自制优化进行了优化,则分析会发生变化。如果应用程序被优化为所有模块都在一个文件中,则每次在上述情况下支付成本情景1时,您将支付成本情景2。
为了完整起见,我会补充说,在您不知道可能要加载的模块的情况下,使用require是必要的。
define([<bunch of other required modules>],
    function (<bunch of other modules variables>) {

    [...]

    require(<requirements unknown ahead of time>, function(m1, m2, ...) {
        m1.foo();
        m2.foo();
        [...]
    });

    [...]
});

1

这取决于您想要的加载行为。

当您需要一个尚未被引用的模块时,将发出HTTP请求以加载所需的模块。如果所需模块已经至少加载了一次,则存储在require的缓存中,因此当您第二次需要该模块时,它将使用缓存值。

因此,这是懒加载或急切加载之间的选择。

require试图解决的问题是每个模块都可以定义其依赖关系-因此,您的第一种方法可能是“最佳实践”。最终,您将希望使用r.js将应用程序构建为单个JS文件。


在我所描述的情境中,所有的“use”函数都需要尽快运行。因此,我并不直接关心惰性加载它们。在第二种方法中,我认为,例如,如果其中一个模块(例如gmaps)需要更长时间才能加载,则其他模块可能可以更快地执行(因此更快地向用户显示一些UI操作)。 - Cosmin SD
据我所知,无论您使用什么方法,只有在所有依赖项加载完毕之后,才会运行您的任何代码-因此,如果您事先定义了所有依赖关系或未定义,实际上并没有什么区别。我刚意识到我的大部分答案都是虚假的,因为最终在所有依赖项加载完毕之前,不会运行任何代码。 - Jeff

1
我的观点是第二种方法与requirejs的主要目的相悖,即分离不同的程序逻辑。如果你把代码放在一个“模块”中,为什么还要使用requirejs呢?就性能而言,重要的是定义你对性能的定义。如果你认为性能是代码执行的速度,那么不会有明显的差异。只有所有所需的模块加载完毕后,代码才会执行。另一方面,如果你所说的性能是指代码执行开始的时间,那么显然在第二种方法中,由于需要额外请求文件以定位所需代码,会有额外的开销。

编辑:

仔细看了问题后,我回答的第一部分并不适用。实际上,该代码引用了外部模块,因此第二个选项并不违反requirejs库的目的,只要组合其依赖项的模块不试图做太多事情,这实际上是更好的方法。


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