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

时间:2016-12-03 08:51:30

标签: javascript node.js validation ecmascript-6

我正在尝试为"可链接"编写模块。 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()

2 个答案:

答案 0 :(得分:4)

我没有太多时间来看这个,但我确实注意到一个相当大的问题。当next!validator.valid时,您有一个单分支if语句导致true被称为两次。通常,单分支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函数表达得更好......

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时总是需要 True False 分支。

以下是我编写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没有返回值,因此使用三元可以很好地处理隐式返回
  • 三元表达式将您限制为每个分支一个表达式 - 再次强制您保持代码简单
  • 三元表达式强制您同时使用true false分支,以便始终处理谓词的两个结果

是的,没有什么可以阻止你使用()将多个表达式组合成一个表达式,但重点不是将每个函数都减少为单个表达式 - 它更适合于使用它时理想而且很好用解决了。如果您觉得可读性受到影响,您可以使用if (...) { return ... } else { return ... }来获得熟悉且友好的语法/风格。

答案 1 :(得分:3)

方法链接溢出

来自full code paste

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

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

isolated test中,我们显示这与递归无关或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 schema。不是用代码编写验证,而是用数据声明地写它。

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