繁体   English   中英

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

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

我正在尝试编写用于“可链接” 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!');
});

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;

};

该代码可用于短链,并返回适当的错误对象。 但是,如果我链接了很多方法,请说:

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引发RangeError: Maximum call stack size exceeded 请注意,我将异步回调.go(next)包装在process.nextTick()

我没有太多时间去看这个,但是我确实注意到了一个很大的问题。 您有一个单分支if语句,当!validator.validtrue时,该语句导致next调用两次 通常,单分支if语句具有代码味道。

这可能不是您遇到堆栈溢出的原因,但这可能是罪魁祸首。

(代码更改以粗体显示

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

有些人用return用欺骗if太。 这也可以,但是很烂

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

我认为这样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);
  }
};

其他备注

您的代码中也有其他单分支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;
};

这种说法不那么令人反感,但我仍然认为,一旦您对if语句始终包含else表示赞赏,就很难推理了。 考虑强制两个分支的三元运算符( ?: :)。 还可以考虑使用诸如Scheme之类的语言,其中在使用if时始终需要TrueFalse分支。

这就是我写你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
  }
};

这是一个自以为是的话,但我认为这是值得考虑的。 当您必须返回此代码并稍后阅读时,您将感谢我。 当然,一旦您的代码采用这种格式,就可以加深理解,以消除很多语法上的重复

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

好处

  • 隐式return有效地迫使您在函数中使用单个表达式–这意味着您不能(轻松)使函数过于复杂
  • 三元表达式是表达式 ,而不是语句if没有返回值,则使用三元表达式与隐式return效果很好
  • 三元表达式将您限制为每个分支一个表达式–再次,迫使您保持代码简单
  • 三元表达式迫使您同时使用true false分支,以便您始终处理谓词的两个结果

是的,没有什么可以阻止您使用()将多个表达式组合为一个表达式,但是重点并不是要把每个函数都简化为一个表达式–当它起作用时,它是一种理想且好用的方法。 如果任何时候您认为可读性受到影响,则可以诉诸if (...) { return ... } else { return ... }以获取熟悉和友好的语法/样式。

方法链溢出

完整的代码粘贴

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

您根本无法在一个表达式中链接那么多方法。

隔离的测试中 ,我们表明这与递归或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

在OSX上使用64位Chrome,在发生堆栈溢出之前,方法链接限制为6253 这可能因实施而异。


横向思考

方法链DSL似乎是为数据指定验证属性的好方法。 在给定的验证表达式中,您不太可能需要链接几十行,因此您不必太担心限制。

除此之外,完全不同的解决方案可能会更好。 立即想到的一个示例是JSON模式 与其使用代码编写验证,不如使用数据声明式编写。

这是一个快速的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"]
}

实际上,架构的大小没有限制,因此这应该适合解决您的问题。

其他优点

  • 模式是可移植的,因此您应用程序的其他区域(例如测试)或数据的其他使用者可以使用它
  • 架构是JSON,因此是一种熟悉的格式,用户无需学习新的语法或API

暂无
暂无

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

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