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

我试图写一个“可链式”Express.jsvalidation模块:

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 。 请注意,我在process.nextTick()包装了asynchronouscallback.go(next) process.nextTick()

方法溢出

从你的完整的代码粘贴

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

你根本不能在单个expression式中链接许多方法。

在一个孤立的testing中 ,我们显示这与recursion或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似乎是为数据指定validation属性的好方法。 在给定的validationexpression式中,你不太可能需要链接几十行,所以你不应该太担心这个限制。

除此之外,完全不同的解决scheme可能会更好。 立即想到的一个例子是JSON模式 。 用代码编写validation,而不是用数据声明式编写。

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

对于你的模式可以有多大是没有限制的,所以这应该适合解决你的问题。

其他优点

  • 架构是可移植的,所以您的应用程序的其他领域(例如testing)或其他数据消费者可以使用它
  • 架构是JSON,所以它是一个熟悉的格式,用户不需要学习新的语法或API

我没有太多的时间来看这个,但我注意到一个很大的问题。 当!validator.validtrue时,你有一个单独的分支if语句,导致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作弊。 这也有效,但它很糟糕

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

我认为整个gofunction是这样expression的更好…

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

这一个是不太冒犯的,但是我仍然认为,一旦你得到一个总是有else陈述的赞赏,就很难推理。 考虑强制两个分支的三元运算符( ?: :)。 还要考虑像Scheme这样的语言,在使用if时总是需要TrueFalse分支。

以下是我如何写你presentfunction

 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有效地迫使你在你的函数中使用一个expression式 – 这意味着你不能(容易)使你的函数过于复杂
  • 三元expression式是一个expression式 ,而不是一个语句if没有返回值,那么使用三元expression式与隐式返回
  • 三元expression式限制你每个分支一个expression式 – 再一次,迫使你保持你的代码简单
  • 三元expression式迫使你使用false分支,这样你总是能够处理谓词的两个结果

是的,没有什么能够阻止你使用()将多个expression式合并成一个expression式,但是并不是要把每个expression式都简化为一个expression式,而是在一个expression式中使用它更加理想,更好。 如果您在任何时候感觉可读性受到影响,那么可以使用if (...) { return ... } else { return ... }来获得熟悉且友好的语法/样式。