3

在节点中,我看到在模块中初始化的全局变量在请求之间变得混杂[一个请求所做的更改会影响另一个请求]。例如:

一个.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 函数,并且每次都会有不同的范围对象。

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

但我发现情况并非如此。谁能解释节点如何以特定的方式处理函数的模块导出[它处理缓存模块导出对象的范围],以及如何克服模块内请求之间的共享全局变量?

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

4

3 回答 3

11

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

当您调用时require,节点要么同步从磁盘读取,要么返回模块的缓存导出对象。读取文件时,它遵循一组有些复杂的规则来准确确定读取的是哪个文件,但是一旦它有了路径:

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

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

下次您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了变化。

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

一个.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每个请求中获得一个全新且独立的范围。

于 2013-05-08T16:28:57.947 回答
3

这实际上取决于您在此过程中何时分配名称。

如果在将名称分配给调用 requestListener 之间,有一个异步方法,那么即使 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 中处理 Async 函数与多线程编程非常相似,您必须注意封装数据。一般来说,你应该尽量避免使用 Global 对象,如果你确实使用它们,它们应该是:不可变的或自包含的。

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

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

于 2013-05-08T14:51:39.870 回答
0

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

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

于 2013-05-08T14:31:32.043 回答