简体   繁体   English

Node.js —即使使用process.nextTick(),也超出了最大调用堆栈大小

[英]Node.js — Maximum call stack size exceeded, even with process.nextTick()

I'm trying to write a module for "chainable" Express.js validation: 我正在尝试编写用于“可链接” Express.js验证的模块:

const validatePost = (req, res, next) => {
  validator.validate(req.body)
    .expect('name.first')
      .present('The parameter is required')
      .string('The parameter must be a string')
    .go(next);
};

router.post('/', validatePost, (req, res, next) => {
  return res.send('Validated!');
});

The code of validator.validate (simplified for brevity): validator.validate的代码(为简便起见简化):

const validate = (data) => {

  let validation;

  const expect = (key) => {
    validation.key = key;

    // Here I get the actual value, but for testing purposes of .present() and
    // .string() chainable methods it returns a random value from a string,
    // not string and an undefined
    validation.value = [ 'foo', 123, void 0 ][Math.floor(Math.random() * 3)];
    return validation;
  };

  const present = (message) => {
    if (typeof validation.value === 'undefined') {
      validation.valid = false;
      validation.errors.push({ key: validation.key, message: message });
    }
    return validation;
  };

  const string = (message) => {
    if (typeof validation.value !== 'string') {
      validation.valid = false;
      validation.errors.push({ key: validation.key, message: message });
    }
    return validation;
  };

  const go = (next) => {
    if (!validation.valid) {
      let error = new Error('Validation error');
      error.name = 'ValidationError';
      error.errors = validation.errors;

      // I even wrap async callbacks in process.nextTick()
      process.nextTick(() => next(error));
    }
    process.nextTick(next);
  };

  validation = {
    valid: true,
    data: data,
    errors: [],
    expect: expect,
    present: present,
    string: string,
    go: go
  };

  return validation;

};

The code works fine for short chains, returning a proper error object. 该代码可用于短链,并返回适当的错误对象。 However if I chain a lot of methods, say: 但是,如果我链接了很多方法,请说:

const validatePost = (req, res, next) => {
  validator.validate(req.body)
    .expect('name.first')
      .present('The parameter is required')
      .string('The parameter must be a string')
    .expect('name.first') // Same for testing
      .present('The parameter is required')
      .string('The parameter must be a string')
    // [...] 2000 times
    .go(next);
};

Node.js throws RangeError: Maximum call stack size exceeded . Node.js引发RangeError: Maximum call stack size exceeded Note that I wrapped my async callback .go(next) in a process.nextTick() . 请注意,我将异步回调.go(next)包装在process.nextTick()

I didn't have a lot of time to look at this, but I did notice a pretty big problem. 我没有太多时间去看这个,但是我确实注意到了一个很大的问题。 You have a single-branch if statement that leads to next being called twice when !validator.valid is true . 您有一个单分支if语句,当!validator.validtrue时,该语句导致next调用两次 In general, single-branch if statements are a code smell. 通常,单分支if语句具有代码味道。

This might not be the reason you're experiencing a stack overflow, but it's a likely culprit. 这可能不是您遇到堆栈溢出的原因,但这可能是罪魁祸首。

(Code changes appear in bold ) (代码更改以粗体显示

const go = (next) => {
  if (!validation.valid) {
    let error = new Error('Validation error');
    error.name = 'ValidationError';
    error.errors = validation.errors;
    process.nextTick(() => next(error));
  }
  else {
    process.nextTick(next);
  }
};

Some people use return to cheat with if too. 有些人用return用欺骗if太。 This also works, but it sucks 这也可以,但是很烂

const go = (next) => {
  if (!validation.valid) {
    let error = new Error('Validation error');
    error.name = 'ValidationError';
    error.errors = validation.errors;
    process.nextTick(() => next(error));
    return; // so that the next line doesn't get called too
  }
  process.nextTick(next);
};

I think the entire go function is expressed better like this ... 我认为这样go更好地表达整个go函数...

const go = (next) => {
  // `!` is hard to reason about
  // place the easiest-to-understand, most-likely-to-happen case first
  if (validation.valid) {
    process.nextTick(next)
  }
  // very clear if/else branching
  // there are two possible outcomes and one block of code for each
  else {
    let error = new Error('Validation error');
    error.name = 'ValidationError';
    error.errors = validation.errors;
    // no need to create a closure here
    process.nextTick(() => next(error));
    process.nextTick(next, error);
  }
};

Other remarks 其他备注

You have other single-branch if statements in your code too 您的代码中也有其他单分支if语句

const present = (message) => {
  if (typeof validation.value === 'undefined') {
    // this branch only performs mutations and doesn't return anything
    validation.valid = false;
    validation.errors.push({ key: validation.key, message: message });
  }
  // there is no `else` branch ...

  return validation;
};

This one is less offensive, but I still think it's harder to reason about once you gain an appreciation for if statements that always have an else . 这种说法不那么令人反感,但我仍然认为,一旦您对if语句始终包含else表示赞赏,就很难推理了。 Consider the ternary operator ( ?: ) that forces both branches. 考虑强制两个分支的三元运算符( ?: :)。 Also consider languages like Scheme where a True and False branch are always required when using if . 还可以考虑使用诸如Scheme之类的语言,其中在使用if时始终需要TrueFalse分支。

Here's how I'd write your present function 这就是我写你present函数的方式

const present = (message) => {
  if (validation.value === undefined) {
    // True branch returns
    return Object.assign(validation, {
      valid: false,
      errors: [...validation.errors, { key: validation.key, message }]
    })
  }
  else {
    // False branch returns
    return validation
  }
};

It's an opinionated remark, but I think it's one worth considering. 这是一个自以为是的话,但我认为这是值得考虑的。 When you have to return to this code and read it later, you'll thank me. 当您必须返回此代码并稍后阅读时,您将感谢我。 Of course once your code is in this format, you can sugar the hell out of it to remove a lot of syntactic boilerplate 当然,一旦您的代码采用这种格式,就可以加深理解,以消除很多语法上的重复

const present = message =>
  validation.value === undefined
    ? Object.assign(validation, {
        valid: false,
        errors: [...validation.errors, { key: validation.key, message }]
      })
    : validation

Advantages 好处

  • Implicit return effectively forces you to use a single expression in your function – this means you cannot (easily) over-complicate your functions 隐式return有效地迫使您在函数中使用单个表达式–这意味着您不能(轻松)使函数过于复杂
  • Ternary expression is an expression , not a statementif does not have a return value so use of ternary works well with the implicit return 三元表达式是表达式 ,而不是语句if没有返回值,则使用三元表达式与隐式return效果很好
  • Ternary expression limits you to one expression per branch – again, forces you to keep your code simple 三元表达式将您限制为每个分支一个表达式–再次,迫使您保持代码简单
  • Ternary expression forces you to use both true and false branches so that you always handle both outcomes of the predicate 三元表达式迫使您同时使用true false分支,以便您始终处理谓词的两个结果

And yes, there's nothing stopping you from using () to combine multiple expressions into one, but the point isn't to reduce every function to a single expression – it's more of an ideal and nice to use when it works out. 是的,没有什么可以阻止您使用()将多个表达式组合为一个表达式,但是重点并不是要把每个函数都简化为一个表达式–当它起作用时,它是一种理想且好用的方法。 If at any time you feel readability was affected, you can resort to if (...) { return ... } else { return ... } for a familiar and friendly syntax/style. 如果任何时候您认为可读性受到影响,则可以诉诸if (...) { return ... } else { return ... }以获取熟悉和友好的语法/样式。

Method Chaining Overflow 方法链溢出

From your full code paste 完整的代码粘贴

validate({ name: { last: 'foo' }})
    // Duplicate this line ~2000 times for error
    .expect('name.first').present().string()
    .go(console.log);

You simply cannot chain that many methods in a single expression. 您根本无法在一个表达式中链接那么多方法。

In an isolated test , we show that this has nothing to do with recursion or process.nextTick 隔离的测试中 ,我们表明这与递归或process.nextTick无关process.nextTick

class X {
  foo () {
    return this
  }
}

let x = new X()

x.foo().foo().foo().foo().foo().foo().foo().foo().foo().foo()
.foo().foo().foo().foo().foo().foo().foo().foo().foo().foo()
.foo().foo().foo().foo().foo().foo().foo().foo().foo().foo()
...
.foo().foo().foo().foo().foo().foo().foo().foo().foo().foo()

// RangeError: Maximum call stack size exceeded

Using 64-bit Chrome on OSX, the method chaining limit is 6253 before a stack overflow happens. 在OSX上使用64位Chrome,在发生堆栈溢出之前,方法链接限制为6253 This likely varies per implementation. 这可能因实施而异。


Lateral thinking 横向思考

A method-chaining DSL seems like a nice way to specify validation properties for your data. 方法链DSL似乎是为数据指定验证属性的好方法。 It's unlikely you'd need to chain more than a couple dozen lines in a given validation expression, so you shouldn't be too worried about the limit. 在给定的验证表达式中,您不太可能需要链接几十行,因此您不必太担心限制。

Aside from that, a completely different solution might be altogether better. 除此之外,完全不同的解决方案可能会更好。 One example that immediately comes to mind is JSON schema . 立即想到的一个示例是JSON模式 Instead of writing validation with code, would write it declaratively with data. 与其使用代码编写验证,不如使用数据声明式编写。

Here's a quick JSON schema example 这是一个快速的JSON模式示例

{
  "title": "Example Schema",
  "type": "object",
  "properties": {
    "firstName": {
      "type": "string"
    },
    "lastName": {
      "type": "string"
    },
    "age": {
      "description": "Age in years",
      "type": "integer",
      "minimum": 0
    }
  },
  "required": ["firstName", "lastName"]
}

There's effectively no limit on how big your schema can be, so this should be suitable to solve your problem. 实际上,架构的大小没有限制,因此这应该适合解决您的问题。

Other advantages 其他优点

  • Schema is portable so other areas of your app (eg testing) or other consumers of your data can use it 模式是可移植的,因此您应用程序的其他区域(例如测试)或数据的其他使用者可以使用它
  • Schema is JSON so it's a familiar format and users don't need to learn new syntax or API 架构是JSON,因此是一种熟悉的格式,用户无需学习新的语法或API

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM