节点js模块中的全局变量

时间:2013-05-06 09:58:51

标签: node.js module scope

在节点I中,看到模块内部全局初始化的变量正在混淆(一个请求完成的更改会影响另一个请求)。 对于Ex:

a.js

var a;
function printName(req, res) {
  //get param `name` from url;
  a = name;
  res.end('Hi '+a);
}
module.exports.printName = printName;

index.js

//Assume all createServer stuffs are done and following function as a CB to createServer
function requestListener(req, res) {
  var a = require('a');
  a.printName(req, res);
}

根据我的假设,每当新请求到达节点时,就会执行从模块'a'导出的printName函数,并且每次都会有不同的范围对象。

因此,在模块中包含全局内容不会影响它们的请求。

但我发现事实并非如此。任何人都可以解释节点如何处理特定的函数模块导出[它处理缓存模块导出对象的范围]以及如何跨模块中的请求克服这个共享的全局变量?

编辑[我们按请求执行异步任务]: 在我们的实时系统中快速请求。其中基本上查询redis并响应请求。我们看到错误的响应映射到错误的请求(回复[存储在模块中的全局变量]中,redis查找错误地映射到diff req)。此外,我们还有一些默认值作为全局变量,可以根据请求参数覆盖。这也搞砸了

3 个答案:

答案 0 :(得分:11)

了解正在发生的事情的第一步是了解幕后发生的事情。从语言的角度来看,节点模块并没有什么特别之处。 “魔力”来自于require时节点如何从磁盘加载文件。

当您调用require时,节点可以同步从磁盘读取或返回模块的缓存导出对象。在读取文件时,它会跟随a set of somewhat complex rules确定读取哪个文件,但一旦有了路径:

  1. 检查require.cache[moduleName]是否存在。如果是,请返回并停止。
  2. code = fs.readFileSync(path)
  3. Wrap(连接)code,字符串(function (exports, require, module, __filename, __dirname) { ... });
  4. eval包装的代码并调用匿名包装函数。

    var module = { exports: {} };
    eval(code)(module.exports, require, module, path, pathMinusFilename);
    
  5. module.exports保存为require.cache[moduleName]

  6. 下次require同一模块时,节点只返回缓存的exports对象。 (这是一件非常好的事情,因为初始加载过程缓慢且同步。)

    所以现在你应该能够看到:

    • 模块中的顶级代码只执行一次。
    • 因为它实际上是在匿名函数中执行的:
      • '全局'变量实际上不是全局变量(除非您明确指定给global或不使用var调整变量的范围)
      • 这是模块获取本地范围的方式。

    在您的示例中,您require模块 a 用于每个请求,但您实际上是在所有请求中共享相同的模块范围,因为模块上面概述了缓存机制。对printName的每次调用都在其作用域链中共享相同的a(即使printName本身在每次调用时都获得了新作用域。)

    现在你在问题中的文字代码中,这没关系:你设置a然后在下一行使用它。控件永远不会离开printName,因此共享a这一事实无关紧要。我的猜测是你的真实代码看起来更像:

    var a;
    function printName(req, res) {
      //get param `name` from url;
      a = name;
      getSomethingFromRedis(function(result) {
          res.end('Hi '+a);
      });
    }
    module.exports.printName = printName;
    

    我们遇到问题,因为控件 离开printName。回调最终会触发,但在此期间另一个请求已更改a

    你可能想要更像这样的东西:

    <强> a.js

    module.exports = function A() {
        var a;
        function printName(req, res) {
          //get param `name` from url;
          a = name;
          res.end('Hi '+a);
        }
    
        return {
            printName: printName
        };
    }
    

    <强> index.js

    var A = require('a');
    function requestListener(req, res) {
      var a = A();
      a.printName(req, res);
    }
    

    这样,您就可以在A内为每个请求获得一个全新且独立的范围。

答案 1 :(得分:3)

这实际上取决于您在流程中何时指定名称。

如果在为调用requestListener分配名称之间有一个异步方法,那么你就会有“竞争条件”(IE两个线程同时改变同一个对象),即使node.js是单线程的。
这是因为当异步方法在后台运行时,node.js将开始处理新请求。

例如,查看以下序列:

request1 starts processing, sets name to 1
request1 calls an async function 
node.js frees the process, and handles the next request in queue.
request2 starts processing, sets name to 2
request2 calls an async function
node.js frees the process, the async function for request 1 is done, so it calls the callback for this function.
request1 calls requestListener, however at this point name is already set to 2 and not 1.

在Node.js中处理异步函数与多线程编程非常相似,必须注意封装数据。一般来说,你应该尽量避免使用Global对象,如果你使用它们,它们应该是:immutable或self-contained。

全局对象不应该用于在函数之间传递状态(这就是你正在做的事情)。

你的问题的解决方案应该是将名称全局放在一个对象中,建议的位置在请求对象内部,它被传递给请求处理管道中的所有大多数函数(这就是connect.js,express .js和所有中间件都在做),或者在一个会话中(参见connect.js会话中间件),它允许你在来自同一个用户的不同请求之间保存数据。

答案 2 :(得分:0)

模块被设计为运行一次并缓存模块,结合节点的异步性质意味着res.end('Hi '+a)之前约有50%的时间a = name执行(因为a已知)。

归根结底,它归结为JavaScript的一个简单事实:全球变量是邪恶的。我不会使用全局,除非它永远不会被请求覆盖。